From 09b0c1e9a5c71d5bc9d2c8818fef0a73a2350eba Mon Sep 17 00:00:00 2001 From: Andrey Belonogov Date: Fri, 2 Oct 2026 16:59:15 -0700 Subject: [PATCH 1/9] feat(maui): upgrade to OTel Java 1.66 / OTel Android 1.7 and shade bundled okio/okhttp - Bump OpenTelemetry Java 1.51.0 -> 1.66.0 and OpenTelemetry Android 0.11.0-alpha -> 1.7.0 in observability-android and the MAUI LDObserve native module; migrate to the split SessionProvider/SessionPublisher API, RumBuilder, and the new AttributeType.VALUE / ValueType.EMPTY cases. - Pin kotlinx-coroutines, kotlin-stdlib, androidx.core and okhttp to versions the current Kotlin 2.0.21 / AGP 8.x toolchain can build against. - Drop the Org.Jetbrains.Kotlinx.KotlinxSerializationJson NuGet dependency. Its JVM package injects a jar from a hardcoded Gradle cache path, which duplicated kotlinx.serialization.json classes in apps using Xamarin.KotlinX.Serialization.Json. - Relocate the okio and okhttp3 classes bundled in ldobserve-otel-bundle.jar under com.launchdarkly.sessionreplay.shaded so they no longer collide with Square.OkIO (R8 'defined multiple times' failure in Release builds, e.g. via Firebase). - Update NativeAndroidDeps.props globs for the renamed OTel Android 1.x artifacts. - Fix ObservabilityBridge for the pageViews -> screenViews rename. Co-authored-by: Cursor --- .../LDObserve.Android.Binding.csproj | 23 +- .../android/native/LDObserve/build.gradle.kts | 200 +++++++++++++++--- .../example/LDObserve/ObservabilityBridge.kt | 5 +- .../xcshareddata/swiftpm/Package.resolved | 8 +- .../observability/LDObservability.Fat.csproj | 17 +- .../observability/NativeAndroidDeps.props | 25 ++- .../lib/build.gradle.kts | 47 +++- .../observability/bridge/LDValueAttributes.kt | 1 + .../observability/client/LDSessionManager.kt | 57 +++-- .../client/LaunchTimeInstrumentation.kt | 4 +- .../client/ObservabilityContext.kt | 4 +- .../client/ObservabilityService.kt | 24 +-- .../otlp/json/OtlpJsonEncoding.kt | 1 + .../otlp/json/common/JsonCommonAdapter.kt | 3 + .../otlp/json/common/OtlpJsonCommonModels.kt | 4 + .../replay/InteractionMoveGrouper.kt | 4 +- .../observability/replay/InteractionSource.kt | 4 +- .../replay/SessionReplayService.kt | 4 +- .../replay/capture/CaptureManager.kt | 4 +- .../android/LDRumSessionManagerAccessor.kt | 17 -- .../replay/InteractionMoveGrouperTest.kt | 6 +- .../observability/replay/SessionReplayTest.kt | 2 +- .../replay/capture/CaptureManagerTest.kt | 4 +- 23 files changed, 335 insertions(+), 133 deletions(-) delete mode 100644 sdk/@launchdarkly/observability-android/lib/src/main/kotlin/io/opentelemetry/android/LDRumSessionManagerAccessor.kt 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..e791c99c4e 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,24 +33,19 @@ - - - - - - - - - - - - - - + + + + + + + + + diff --git a/sdk/@launchdarkly/mobile-dotnet/android/native/LDObserve/build.gradle.kts b/sdk/@launchdarkly/mobile-dotnet/android/native/LDObserve/build.gradle.kts index 887d92c8b6..9c97665f2a 100644 --- a/sdk/@launchdarkly/mobile-dotnet/android/native/LDObserve/build.gradle.kts +++ b/sdk/@launchdarkly/mobile-dotnet/android/native/LDObserve/build.gradle.kts @@ -1,6 +1,21 @@ import java.util.zip.ZipEntry import java.util.zip.ZipFile import java.util.zip.ZipOutputStream +import org.objectweb.asm.ClassReader +import org.objectweb.asm.ClassWriter +import org.objectweb.asm.commons.ClassRemapper +import org.objectweb.asm.commons.Remapper + +// ASM powers the package relocation performed by BundleOtelJarsTask. It is only +// needed on the build classpath; nothing from it is shipped. +buildscript { + repositories { + mavenCentral() + } + dependencies { + classpath("org.ow2.asm:asm-commons:9.9") + } +} plugins { id("com.android.library") @@ -40,6 +55,32 @@ configurations { create("copyDependencies") } +// Mirrors the pins in observability-android: OTel Android 1.7 drags in kotlinx-coroutines 1.11.0 +// (Kotlin 2.2 metadata, unreadable by the 2.0.21 compiler) and androidx.core 1.19.0 (needs AGP 9.1 +// / compileSdk 37). Both are held back to the newest releases this toolchain can build against. +configurations.all { + resolutionStrategy.eachDependency { + val name = requested.name + // OTel Android 1.7 depends on kotlin-stdlib 2.4.10, whose metadata this module's 2.0.21 + // compiler cannot read. Hold the Kotlin runtime at the compiler's own version. + val isKotlinRuntime = name.startsWith("kotlin-stdlib") || + name == "kotlin-reflect" || + name.startsWith("kotlin-test") + if (requested.group == "org.jetbrains.kotlin" && isKotlinRuntime) { + useVersion("2.0.21") + } + if (requested.group == "org.jetbrains.kotlinx" && name.startsWith("kotlinx-coroutines")) { + useVersion("1.10.2") + } + if (requested.group == "androidx.core" && (name == "core" || name == "core-ktx")) { + useVersion("1.16.0") + } + if (requested.group == "com.squareup.okhttp3") { + useVersion("5.2.1") + } + } +} + dependencies { implementation("androidx.core:core-ktx:1.15.0") "copyDependencies"("androidx.core:core-ktx:1.15.0") @@ -52,45 +93,52 @@ dependencies { // TODO: revise these versions to be as old as usable for compatibility // OpenTelemetry JARs copied here are filtered for NuGet in observability/LDObservability.Fat.csproj (autoconfigure vs autoconfigure-spi). - implementation("io.opentelemetry:opentelemetry-api:1.51.0") - "copyDependencies"("io.opentelemetry:opentelemetry-api:1.51.0") + implementation("io.opentelemetry:opentelemetry-api:1.66.0") + "copyDependencies"("io.opentelemetry:opentelemetry-api:1.66.0") - implementation("io.opentelemetry:opentelemetry-sdk:1.51.0") - "copyDependencies"("io.opentelemetry:opentelemetry-sdk:1.51.0") + implementation("io.opentelemetry:opentelemetry-sdk:1.66.0") + "copyDependencies"("io.opentelemetry:opentelemetry-sdk:1.66.0") - implementation("io.opentelemetry:opentelemetry-exporter-otlp:1.51.0") - "copyDependencies"("io.opentelemetry:opentelemetry-exporter-otlp:1.51.0") + implementation("io.opentelemetry:opentelemetry-exporter-otlp:1.66.0") + "copyDependencies"("io.opentelemetry:opentelemetry-exporter-otlp:1.66.0") - implementation("io.opentelemetry:opentelemetry-exporter-sender-okhttp:1.51.0") - "copyDependencies"("io.opentelemetry:opentelemetry-exporter-sender-okhttp:1.51.0") + implementation("io.opentelemetry:opentelemetry-exporter-sender-okhttp:1.66.0") + "copyDependencies"("io.opentelemetry:opentelemetry-exporter-sender-okhttp:1.66.0") - implementation("io.opentelemetry:opentelemetry-exporter-logging-otlp:1.51.0") - "copyDependencies"("io.opentelemetry:opentelemetry-exporter-logging-otlp:1.51.0") + implementation("io.opentelemetry:opentelemetry-exporter-logging-otlp:1.66.0") + "copyDependencies"("io.opentelemetry:opentelemetry-exporter-logging-otlp:1.66.0") - implementation("io.opentelemetry:opentelemetry-sdk-metrics:1.51.0") - "copyDependencies"("io.opentelemetry:opentelemetry-sdk-metrics:1.51.0") + implementation("io.opentelemetry:opentelemetry-sdk-metrics:1.66.0") + "copyDependencies"("io.opentelemetry:opentelemetry-sdk-metrics:1.66.0") - implementation("io.opentelemetry:opentelemetry-sdk-logs:1.51.0") - "copyDependencies"("io.opentelemetry:opentelemetry-sdk-logs:1.51.0") + implementation("io.opentelemetry:opentelemetry-sdk-logs:1.66.0") + "copyDependencies"("io.opentelemetry:opentelemetry-sdk-logs:1.66.0") // TODO: Evaluate risks associated with incubator APIs - implementation("io.opentelemetry:opentelemetry-api-incubator:1.51.0-alpha") - "copyDependencies"("io.opentelemetry:opentelemetry-api-incubator:1.51.0-alpha") + implementation("io.opentelemetry:opentelemetry-api-incubator:1.66.0-alpha") + "copyDependencies"("io.opentelemetry:opentelemetry-api-incubator:1.66.0-alpha") // OTEL Android - implementation("io.opentelemetry.android:core:0.11.0-alpha") - "copyDependencies"("io.opentelemetry.android:core:0.11.0-alpha") + implementation("io.opentelemetry.android:core:1.7.0-alpha") + "copyDependencies"("io.opentelemetry.android:core:1.7.0-alpha") - implementation("io.opentelemetry.android:session:0.11.0-alpha") - "copyDependencies"("io.opentelemetry.android:session:0.11.0-alpha") + implementation("io.opentelemetry.android:session:1.7.0") + "copyDependencies"("io.opentelemetry.android:session:1.7.0") + + // `OpenTelemetryRum` and `AndroidInstrumentation` moved out of :core in 1.x. + implementation("io.opentelemetry.android:agent-api:1.7.0") + "copyDependencies"("io.opentelemetry.android:agent-api:1.7.0") + + implementation("io.opentelemetry.android.instrumentation:android-instrumentation:1.7.0") + "copyDependencies"("io.opentelemetry.android.instrumentation:android-instrumentation:1.7.0") // OTEL Android Instrumentations - implementation("io.opentelemetry.android.instrumentation:crash:0.11.0-alpha") - "copyDependencies"("io.opentelemetry.android.instrumentation:crash:0.11.0-alpha") + implementation("io.opentelemetry.android.instrumentation:crash:1.7.0-alpha") + "copyDependencies"("io.opentelemetry.android.instrumentation:crash:1.7.0-alpha") - implementation("io.opentelemetry.android:android-agent:0.11.0-alpha") - "copyDependencies"("io.opentelemetry.android:android-agent:0.11.0-alpha") + implementation("io.opentelemetry.android:android-agent:1.7.0") + "copyDependencies"("io.opentelemetry.android:android-agent:1.7.0") } // Custom task: merges all transitive JARs (OTel + okhttp + okio + ...) into a @@ -151,6 +199,97 @@ abstract class BundleOtelJarsTask : DefaultTask() { @get:org.gradle.api.tasks.OutputDirectory abstract val outputServicesDir: org.gradle.api.file.DirectoryProperty + // Okio and OkHttp are moved under a private prefix before being written to + // the bundle. + // + // They have to be shipped: the OTLP exporters resolve their transport at + // runtime through ServiceLoader, the only HttpSenderProvider we register is + // the OkHttp one, and OkHttp's entire I/O layer is Okio (its disk-buffering + // and protobuf code depends on okio.ByteString directly too). Drop either and + // the provider fails to initialise, which is the "No HttpSenderProvider found + // on classpath" crash. + // + // But apps routinely acquire their own copy from unrelated bindings -- + // Square.OkIO arrives transitively through AndroidX DataStore, which Firebase + // depends on -- and two definitions of the same class is a hard R8 failure in + // Release. Relocating ours makes the collision impossible instead of relying + // on the app's graph to stay clear. + private val shadedPrefix = "com/launchdarkly/sessionreplay/shaded/" + private val relocatedPackages = listOf("okio", "okhttp3") + + private val pathRelocations: List> = + relocatedPackages.map { "$it/" to "$shadedPrefix$it/" } + private val nameRelocations: List> = + relocatedPackages.map { "$it." to "${shadedPrefix.replace('/', '.')}$it." } + + /** Relocates a JAR entry path. Anchored at the start, so only the owning package moves. */ + private fun relocateEntryName(name: String): String { + pathRelocations.forEach { (from, to) -> + if (name.startsWith(from)) return to + name.removePrefix(from) + } + return name + } + + /** + * Relocates package references inside a string constant. + * + * Needed because some references never appear as bytecode class references. + * OkHttp builds the path of its public-suffix resource by concatenating the + * literal "okhttp3/internal/publicsuffix/" with a simple class name, so a + * remapper that only rewrote class references would relocate the resource but + * leave the lookup pointing at the old path. + * + * Slashed forms are matched at a delimiter so JVM descriptors embedded in + * strings ("Lokio/Path;") relocate too; dotted forms are matched only at the + * start, where Kotlin's @JvmName and ReplaceWith metadata put them, since an + * unanchored "okio." would be too eager. + */ + private fun relocateString(value: String): String { + var result = value + pathRelocations.forEach { (from, to) -> + if (!result.contains(from)) return@forEach + val sb = StringBuilder(result.length) + var cursor = 0 + while (true) { + val at = result.indexOf(from, cursor) + if (at < 0) { + sb.append(result, cursor, result.length) + break + } + // 'L' and '[' precede a type in a descriptor; the rest are plain separators. + val preceding = if (at == 0) null else result[at - 1] + val atBoundary = preceding == null || preceding in "L[(;,/ <>" + sb.append(result, cursor, at) + sb.append(if (atBoundary) to else from) + cursor = at + from.length + } + result = sb.toString() + } + nameRelocations.forEach { (from, to) -> + if (result.startsWith(from)) result = to + result.removePrefix(from) + } + return result + } + + /** Rewrites class references, descriptors, signatures and string constants. */ + private fun relocateClass(bytes: ByteArray): ByteArray { + // The no-arg constructor is deprecated in ASM 9.9 but is the only one that + // exists in the older ASM that Gradle/AGP put on the runtime classpath. + @Suppress("DEPRECATION") + val remapper = object : Remapper() { + override fun map(internalName: String): String = relocateEntryName(internalName) + + override fun mapValue(value: Any?): Any? = + if (value is String) relocateString(value) else super.mapValue(value) + } + val writer = ClassWriter(0) + // Flag 0: frames are copied through and remapped rather than recomputed. + // COMPUTE_FRAMES would need to load the referenced types, which are not + // on this build's classpath. + ClassReader(bytes).accept(ClassRemapper(writer, remapper), 0) + return writer.toByteArray() + } + @org.gradle.api.tasks.TaskAction fun bundle() { val out = outputJar.get().asFile @@ -190,9 +329,12 @@ abstract class BundleOtelJarsTask : DefaultTask() { continue } - if (!classOrResourceEntries.containsKey(name)) { + val outName = relocateEntryName(name) + if (!classOrResourceEntries.containsKey(outName)) { zip.getInputStream(entry).use { input -> - classOrResourceEntries[name] = input.readBytes() + val raw = input.readBytes() + classOrResourceEntries[outName] = + if (name.endsWith(".class")) relocateClass(raw) else raw } } } @@ -201,11 +343,15 @@ abstract class BundleOtelJarsTask : DefaultTask() { // Merge once, write twice: the same content goes into both the // bundle JAR and the standalone files on disk. + // Provider names are relocated as well. None of the SPIs we ship live in a + // relocated package today (they are all io.opentelemetry.*), so this is a + // no-op now, but it keeps the registrations correct if that ever changes. val mergedServiceFiles = serviceFiles.mapValues { (_, contents) -> contents .flatMap { it.lineSequence() } .map { it.trim() } .filter { it.isNotEmpty() && !it.startsWith("#") } + .map { relocateString(it) } .distinct() .joinToString("\n") + "\n" } @@ -249,6 +395,8 @@ abstract class BundleOtelJarsTask : DefaultTask() { logger.lifecycle( "ldobserve-otel-bundle: ${classOrResourceEntries.size} class/resource entries, " + + "${classOrResourceEntries.keys.count { it.startsWith(shadedPrefix) }} relocated " + + "under $shadedPrefix, " + "${mergedServiceFiles.size} merged service files (in-JAR + standalone), " + "${inputJars.files.size} input JARs" ) 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 0c080a3540..08190aca91 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 @@ -81,8 +81,9 @@ public class ObservabilityBridge( backendUrl = observability.backendUrl, tracesApi = com.launchdarkly.observability.api.ObservabilityOptions.TracesApi(includeErrors = true, includeSpans = true), metricsApi = com.launchdarkly.observability.api.ObservabilityOptions.MetricsApi.enabled(), - // Page views rely on the OTel `activity` instrumentation AAR, which MAUI no longer ships. - analytics = com.launchdarkly.observability.api.ObservabilityOptions.Analytics(pageViews = false), + // Screen views are reported from the MAUI layer, so the Android-side span is left off + // to avoid double-reporting navigation. + analytics = com.launchdarkly.observability.api.ObservabilityOptions.Analytics(screenViews = false), instrumentations = com.launchdarkly.observability.api.ObservabilityOptions.Instrumentations( crashReporting = false, launchTime = observability.launchTime ), diff --git a/sdk/@launchdarkly/mobile-dotnet/macios/native/LDObserve/LDObserve.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/sdk/@launchdarkly/mobile-dotnet/macios/native/LDObserve/LDObserve.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 59f06947da..193293ef73 100644 --- a/sdk/@launchdarkly/mobile-dotnet/macios/native/LDObserve/LDObserve.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/sdk/@launchdarkly/mobile-dotnet/macios/native/LDObserve/LDObserve.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -6,8 +6,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/launchdarkly/ios-client-sdk.git", "state" : { - "revision" : "8b56cf8a7f74618a5d8e9ef0e4dad543e2f3fed7", - "version" : "11.1.1" + "revision" : "53598a805b5887692cc5748a4208df69e0cdc08d", + "version" : "11.6.2" } }, { @@ -42,8 +42,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/LaunchDarkly/swift-eventsource.git", "state" : { - "revision" : "57051701c58a93603ffa2051f8e9cf0c8cff7814", - "version" : "3.3.0" + "revision" : "c2791e4ecc92ed6b5440b51754e41b69fef9fc4f", + "version" : "3.3.1" } } ], diff --git a/sdk/@launchdarkly/mobile-dotnet/observability/LDObservability.Fat.csproj b/sdk/@launchdarkly/mobile-dotnet/observability/LDObservability.Fat.csproj index 6ea5a41c70..ca454df567 100644 --- a/sdk/@launchdarkly/mobile-dotnet/observability/LDObservability.Fat.csproj +++ b/sdk/@launchdarkly/mobile-dotnet/observability/LDObservability.Fat.csproj @@ -24,22 +24,21 @@ - - - + + - - - + + + - - - + + + diff --git a/sdk/@launchdarkly/mobile-dotnet/observability/NativeAndroidDeps.props b/sdk/@launchdarkly/mobile-dotnet/observability/NativeAndroidDeps.props index 325b34f8a5..a77a25c5dd 100644 --- a/sdk/@launchdarkly/mobile-dotnet/observability/NativeAndroidDeps.props +++ b/sdk/@launchdarkly/mobile-dotnet/observability/NativeAndroidDeps.props @@ -18,20 +18,39 @@ - + + <_LDNativeAAR Include="$(_LDNativeDepsDir)agent-api-*.aar" /> <_LDNativeAAR Include="$(_LDNativeDepsDir)android-agent-*.aar" /> <_LDNativeAAR Include="$(_LDNativeDepsDir)android-instrumentation-*.aar" /> - <_LDNativeAAR Include="$(_LDNativeDepsDir)common-0*.aar" /> + + <_LDNativeAAR Include="$(_LDNativeDepsDir)common-1*.aar" /> <_LDNativeAAR Include="$(_LDNativeDepsDir)common-api-*.aar" /> <_LDNativeAAR Include="$(_LDNativeDepsDir)core-*-alpha.aar" /> <_LDNativeAAR Include="$(_LDNativeDepsDir)anr-*-alpha.aar" /> <_LDNativeAAR Include="$(_LDNativeDepsDir)activity-*-alpha.aar" /> <_LDNativeAAR Include="$(_LDNativeDepsDir)fragment-*-alpha.aar" /> <_LDNativeAAR Include="$(_LDNativeDepsDir)network-*-alpha.aar" /> + <_LDNativeAAR Include="$(_LDNativeDepsDir)screen-orientation-*-alpha.aar" /> <_LDNativeAAR Include="$(_LDNativeDepsDir)services-*-alpha.aar" /> - <_LDNativeAAR Include="$(_LDNativeDepsDir)session-*-alpha.aar" /> + + <_LDNativeAAR Include="$(_LDNativeDepsDir)session-*.aar" /> + <_LDNativeAAR Include="$(_LDNativeDepsDir)sessions-*-alpha.aar" /> <_LDNativeAAR Include="$(_LDNativeDepsDir)slowrendering-*-alpha.aar" /> <_LDNativeAAR Include="$(_LDNativeDepsDir)startup-*-alpha.aar" /> + + <_LDNativeAAR Include="$(_LDNativeDepsDir)semconv-*-alpha.aar" /> + + <_LDNativeAAR Include="$(_LDNativeDepsDir)semconv.aar" /> <_LDNativeAAR Include="$(_LDNativeDepsDir)lib-release.aar" /> diff --git a/sdk/@launchdarkly/observability-android/lib/build.gradle.kts b/sdk/@launchdarkly/observability-android/lib/build.gradle.kts index 7f803fc092..e033acb0a6 100644 --- a/sdk/@launchdarkly/observability-android/lib/build.gradle.kts +++ b/sdk/@launchdarkly/observability-android/lib/build.gradle.kts @@ -31,7 +31,7 @@ val isClientSdkProvidedByHost = // Pin Kotlin runtime artifacts on this build's classpath to match the configured Kotlin // compiler version (2.0.21). Without this, transitive deps such as the -// io.opentelemetry.android:*:0.11.0-alpha modules drag in newer kotlin-stdlib / kotlin-reflect +// io.opentelemetry.android:*:1.7.0 modules drag in newer kotlin-stdlib / kotlin-reflect // whose metadata version the 2.0.21 compiler cannot read, producing // "Module was compiled with an incompatible version of Kotlin" errors at compileDebugKotlin. // @@ -50,6 +50,26 @@ configurations.all { useVersion("2.0.21") because("Align Kotlin runtime artifacts with the project's Kotlin compiler version (2.0.21).") } + // io.opentelemetry.android:*:1.7.0 pulls androidx.lifecycle 2.11.0, which in turn requires + // kotlinx-coroutines 1.11.0. That release carries Kotlin metadata 2.2.0, and the 2.0.21 + // compiler reads at most 2.1.0. 1.10.2 is the newest release still on metadata 2.1.0. + if (requested.group == "org.jetbrains.kotlinx" && name.startsWith("kotlinx-coroutines")) { + useVersion("1.10.2") + because("kotlinx-coroutines 1.11.0 carries Kotlin 2.2 metadata the 2.0.21 compiler cannot read.") + } + // io.opentelemetry.android:common:1.7.0-alpha pulls androidx.core 1.19.0, which refuses to + // build on anything below AGP 9.1 / compileSdk 37. Hold it at the newest release that still + // builds against compileSdk 35 with AGP 8.x. + if (requested.group == "androidx.core" && (name == "core" || name == "core-ktx")) { + useVersion("1.16.0") + because("androidx.core 1.17.0+ requires a compileSdk/AGP combination this project does not target yet.") + } + // Same story for okhttp: the OTel 1.66 okhttp sender asks for 5.5.0, whose Android variant + // requires compileSdk 37. + if (requested.group == "com.squareup.okhttp3") { + useVersion("5.2.1") + because("okhttp-android 5.3.0+ requires compileSdk 37, beyond what AGP 8.x supports here.") + } } } @@ -73,28 +93,33 @@ dependencies { implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.9.0") // TODO: revise these versions to be as old as usable for compatibility - implementation("io.opentelemetry:opentelemetry-api:1.51.0") - implementation("io.opentelemetry:opentelemetry-sdk:1.51.0") - implementation("io.opentelemetry:opentelemetry-sdk-metrics:1.51.0") - implementation("io.opentelemetry:opentelemetry-sdk-logs:1.51.0") + implementation("io.opentelemetry:opentelemetry-api:1.66.0") + implementation("io.opentelemetry:opentelemetry-sdk:1.66.0") + implementation("io.opentelemetry:opentelemetry-sdk-metrics:1.66.0") + implementation("io.opentelemetry:opentelemetry-sdk-logs:1.66.0") // Required at runtime by io.opentelemetry.android:core, which uses incubator APIs // internally for the logs bridge. Can be removed once the OTel Android SDK drops this dependency. - implementation("io.opentelemetry:opentelemetry-api-incubator:1.51.0-alpha") + implementation("io.opentelemetry:opentelemetry-api-incubator:1.66.0-alpha") // OTEL Android - implementation("io.opentelemetry.android:core:0.11.0-alpha") - implementation("io.opentelemetry.android:session:0.11.0-alpha") + implementation("io.opentelemetry.android:core:1.7.0-alpha") + implementation("io.opentelemetry.android:session:1.7.0") + + // `OpenTelemetryRum` and `AndroidInstrumentation` moved out of :core in 1.x and are only + // runtime-scoped transitives of it, so they must be declared to be on the compile classpath. + implementation("io.opentelemetry.android:agent-api:1.7.0") + implementation("io.opentelemetry.android.instrumentation:android-instrumentation:1.7.0") // OTEL Android Instrumentations - implementation("io.opentelemetry.android.instrumentation:crash:0.11.0-alpha") + implementation("io.opentelemetry.android.instrumentation:crash:1.7.0-alpha") // NOTE: the `activity` instrumentation is intentionally NOT depended on. It is superseded by // LaunchDarkly's own app/screen lifecycle spans and would otherwise double-report; we also // defensively suppress it by name (see ObservabilityService.createOtelRumConfig). // Use JUnit Jupiter for testing. // Testing exporters for telemetry inspection - testImplementation("io.opentelemetry:opentelemetry-sdk-testing:1.51.0") + testImplementation("io.opentelemetry:opentelemetry-sdk-testing:1.66.0") testImplementation(platform("org.junit:junit-bom:5.13.4")) testImplementation("org.junit.jupiter:junit-jupiter") testRuntimeOnly("org.junit.platform:junit-platform-launcher") @@ -104,7 +129,7 @@ dependencies { testImplementation("io.mockk:mockk:1.14.5") testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.9.0") - testFixturesApi("io.opentelemetry:opentelemetry-sdk-testing:1.51.0") + testFixturesApi("io.opentelemetry:opentelemetry-sdk-testing:1.66.0") testFixturesImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.9.0") } diff --git a/sdk/@launchdarkly/observability-android/lib/src/main/kotlin/com/launchdarkly/observability/bridge/LDValueAttributes.kt b/sdk/@launchdarkly/observability-android/lib/src/main/kotlin/com/launchdarkly/observability/bridge/LDValueAttributes.kt index a2c3daedd7..d5d104038b 100644 --- a/sdk/@launchdarkly/observability-android/lib/src/main/kotlin/com/launchdarkly/observability/bridge/LDValueAttributes.kt +++ b/sdk/@launchdarkly/observability-android/lib/src/main/kotlin/com/launchdarkly/observability/bridge/LDValueAttributes.kt @@ -112,4 +112,5 @@ private fun reKey(original: AttributeKey<*>, newName: String): AttributeKey<*> = AttributeType.BOOLEAN_ARRAY -> AttributeKey.booleanArrayKey(newName) AttributeType.LONG_ARRAY -> AttributeKey.longArrayKey(newName) AttributeType.DOUBLE_ARRAY -> AttributeKey.doubleArrayKey(newName) + AttributeType.VALUE -> AttributeKey.valueKey(newName) } diff --git a/sdk/@launchdarkly/observability-android/lib/src/main/kotlin/com/launchdarkly/observability/client/LDSessionManager.kt b/sdk/@launchdarkly/observability-android/lib/src/main/kotlin/com/launchdarkly/observability/client/LDSessionManager.kt index 38a378d862..7c9df01a64 100644 --- a/sdk/@launchdarkly/observability-android/lib/src/main/kotlin/com/launchdarkly/observability/client/LDSessionManager.kt +++ b/sdk/@launchdarkly/observability-android/lib/src/main/kotlin/com/launchdarkly/observability/client/LDSessionManager.kt @@ -1,28 +1,29 @@ package com.launchdarkly.observability.client import io.opentelemetry.android.session.Session -import io.opentelemetry.android.session.SessionIdGenerator -import io.opentelemetry.android.session.SessionManager import io.opentelemetry.android.session.SessionObserver +import io.opentelemetry.android.session.SessionProvider +import io.opentelemetry.android.session.SessionPublisher import io.opentelemetry.sdk.common.Clock +import java.security.SecureRandom import java.util.Collections.synchronizedList import kotlin.time.Duration /** - * LaunchDarkly's own [SessionManager], used in place of OpenTelemetry Android's default so we can + * LaunchDarkly's own session source, used in place of OpenTelemetry Android's default so we can * **seed the initial session id** ([initialSessionId]). This lets the native observability instance * adopt a session id created elsewhere (e.g. by the JavaScript SDK on the same device), so that * spans, logs, metrics, and Session Replay all report the same `session.id`. * - * Injected into [io.opentelemetry.android.OpenTelemetryRumBuilder] via - * [io.opentelemetry.android.LDRumSessionManagerAccessor] so it backs OpenTelemetry Android's own - * `session.id` span/log appenders as well — making it the single source of session identity. + * Injected into [io.opentelemetry.android.OpenTelemetryRumBuilder] via `setSessionProvider` so it + * backs OpenTelemetry Android's own `session.id` span/log appenders as well — making it the single + * source of session identity. * * When [initialSessionId] is supplied the session is treated as *custom*: the caller owns the * session lifecycle, so automatic rotation is disabled and the seeded id is used for the lifetime * of this manager (mirrors iOS's `isCustomSession`). * - * Otherwise rotation mirrors OpenTelemetry Android's default `SessionManagerImpl` / + * Otherwise rotation mirrors OpenTelemetry Android's default session manager / * `SessionIdTimeoutHandler`: * - a foreground session never times out; * - a background gap of at least [backgroundInactivityTimeout] rotates the session on next use; @@ -37,15 +38,15 @@ import kotlin.time.Duration * @param backgroundInactivityTimeout Background inactivity after which the session rotates. * @param maxLifetime Absolute maximum session lifetime before rotation. * @param clock Time source; defaults to the OpenTelemetry default clock. - * @param idGenerator Generator for new session ids; defaults to OpenTelemetry's. + * @param idGenerator Generator for new session ids; defaults to a random 128-bit hex id. */ internal class LDSessionManager( initialSessionId: String? = null, private val backgroundInactivityTimeout: Duration, private val maxLifetime: Duration, private val clock: Clock = Clock.getDefault(), - private val idGenerator: SessionIdGenerator = SessionIdGenerator.DEFAULT, -) : SessionManager { + private val idGenerator: () -> String = ::randomSessionId, +) : SessionProvider, SessionPublisher { private val lock = Any() private val observers = synchronizedList(ArrayList()) @@ -60,11 +61,11 @@ internal class LDSessionManager( // Guarded by [lock]. private var session: Session = if (isCustomSession) { - Session.DefaultSession(initialSessionId!!, clock.now()) + LDSession(initialSessionId!!, clock.now()) } else { - // Empty id + (-1) timestamp forces generation on first getSessionId(), exactly like - // the default manager's initial Session.NONE. - Session.NONE + // Empty id + zero timestamp forces generation on first read, exactly like the default + // manager's initial "none" session. + NONE_SESSION } // Timeout bookkeeping, mirroring SessionIdTimeoutHandler. @@ -87,7 +88,7 @@ internal class LDSessionManager( var candidate = session // An externally supplied session id is never rotated; the caller owns its lifecycle. if (!isCustomSession && (sessionHasExpired() || hasTimedOut())) { - candidate = Session.DefaultSession(idGenerator.generateSessionId(), clock.now()) + candidate = LDSession(idGenerator(), clock.now()) } // Bump the inactivity timer after deciding, before notifying (a new span may be created). bump() @@ -106,7 +107,7 @@ internal class LDSessionManager( observer.onSessionStarted(newSession, previousSession) } } - return newSession.getId() + return newSession.id } /** Marks the app as transitioning to the foreground; the next event settles it to foreground. */ @@ -120,7 +121,7 @@ internal class LDSessionManager( } private fun sessionHasExpired(): Boolean { - val elapsed = clock.now() - session.getStartTimestamp() + val elapsed = clock.now() - session.startTimestamp return elapsed >= maxLifetime.inWholeNanoseconds } @@ -146,4 +147,26 @@ internal class LDSessionManager( /** Temporary state for the first event after the app is brought back to the foreground. */ TRANSITIONING_TO_FOREGROUND, } + + private class LDSession( + override val id: String, + override val startTimestamp: Long, + ) : Session + + private companion object { + /** + * Stands in for the default manager's "no session yet" value. The zero start timestamp + * makes the first expiry check rotate immediately, generating the real id lazily. + */ + private val NONE_SESSION: Session = LDSession("", 0L) + } +} + +private val sessionIdRandom = SecureRandom() + +/** Generates a random 128-bit session id, rendered as 32 lowercase hex characters. */ +private fun randomSessionId(): String { + val bytes = ByteArray(16) + sessionIdRandom.nextBytes(bytes) + return bytes.joinToString("") { "%02x".format(it) } } diff --git a/sdk/@launchdarkly/observability-android/lib/src/main/kotlin/com/launchdarkly/observability/client/LaunchTimeInstrumentation.kt b/sdk/@launchdarkly/observability-android/lib/src/main/kotlin/com/launchdarkly/observability/client/LaunchTimeInstrumentation.kt index 8d0be0a959..687fc0e0af 100644 --- a/sdk/@launchdarkly/observability-android/lib/src/main/kotlin/com/launchdarkly/observability/client/LaunchTimeInstrumentation.kt +++ b/sdk/@launchdarkly/observability-android/lib/src/main/kotlin/com/launchdarkly/observability/client/LaunchTimeInstrumentation.kt @@ -11,8 +11,8 @@ import android.view.View import android.view.ViewTreeObserver.OnDrawListener import androidx.activity.FullyDrawnReporterOwner import com.launchdarkly.observability.interfaces.Metric +import io.opentelemetry.android.OpenTelemetryRum import io.opentelemetry.android.instrumentation.AndroidInstrumentation -import io.opentelemetry.android.instrumentation.InstallationContext import io.opentelemetry.api.common.Attributes import java.lang.ref.WeakReference import java.util.WeakHashMap @@ -44,7 +44,7 @@ internal class LaunchTimeInstrumentation( override val name: String = INSTRUMENTATION_SCOPE_NAME - override fun install(ctx: InstallationContext) { + override fun install(context: android.content.Context, rum: OpenTelemetryRum) { application.registerActivityLifecycleCallbacks(this) } diff --git a/sdk/@launchdarkly/observability-android/lib/src/main/kotlin/com/launchdarkly/observability/client/ObservabilityContext.kt b/sdk/@launchdarkly/observability-android/lib/src/main/kotlin/com/launchdarkly/observability/client/ObservabilityContext.kt index 7d2f1a2c4d..ae787e4b89 100644 --- a/sdk/@launchdarkly/observability-android/lib/src/main/kotlin/com/launchdarkly/observability/client/ObservabilityContext.kt +++ b/sdk/@launchdarkly/observability-android/lib/src/main/kotlin/com/launchdarkly/observability/client/ObservabilityContext.kt @@ -5,7 +5,7 @@ import com.launchdarkly.observability.api.ObservabilityOptions import com.launchdarkly.observability.client.screen.ScreenViewEvent import com.launchdarkly.observability.client.screen.ScreenViewManager import com.launchdarkly.observability.context.ObserveLogger -import io.opentelemetry.android.session.SessionManager +import io.opentelemetry.android.session.SessionProvider import io.opentelemetry.api.common.Attributes import kotlinx.coroutines.flow.SharedFlow @@ -17,7 +17,7 @@ data class ObservabilityContext( val options: ObservabilityOptions, val application: Application, val logger: ObserveLogger, - var sessionManager: SessionManager? = null, + var sessionManager: SessionProvider? = null, var resourceAttributes: Attributes = Attributes.empty(), /** * The single touch-capture hook owned by Observability. Session Replay consumes its diff --git a/sdk/@launchdarkly/observability-android/lib/src/main/kotlin/com/launchdarkly/observability/client/ObservabilityService.kt b/sdk/@launchdarkly/observability-android/lib/src/main/kotlin/com/launchdarkly/observability/client/ObservabilityService.kt index fa099f6c45..78e64d3857 100644 --- a/sdk/@launchdarkly/observability-android/lib/src/main/kotlin/com/launchdarkly/observability/client/ObservabilityService.kt +++ b/sdk/@launchdarkly/observability-android/lib/src/main/kotlin/com/launchdarkly/observability/client/ObservabilityService.kt @@ -35,13 +35,13 @@ import com.launchdarkly.observability.sampling.SamplingLogProcessor import com.launchdarkly.observability.traces.EventSpanProcessor import com.launchdarkly.observability.traces.OtlpTraceExporter import com.launchdarkly.observability.util.requireMainThread -import io.opentelemetry.android.LDRumSessionManagerAccessor import io.opentelemetry.android.OpenTelemetryRum import io.opentelemetry.android.OpenTelemetryRumBuilder +import io.opentelemetry.android.RumBuilder import io.opentelemetry.android.config.OtelRumConfig import io.opentelemetry.android.session.Session -import io.opentelemetry.android.session.SessionManager import io.opentelemetry.android.session.SessionObserver +import io.opentelemetry.android.session.SessionProvider import io.opentelemetry.api.common.AttributeKey import io.opentelemetry.api.common.Attributes import io.opentelemetry.api.logs.Logger @@ -106,7 +106,7 @@ class ObservabilityService( private val otelRUM: OpenTelemetryRum // LaunchDarkly's own session manager. Owns session identity for all signals (it also backs the - // RUM SDK's `session.id` appenders via LDRumSessionManagerAccessor) and can be seeded with + // RUM SDK's `session.id` appenders via `setSessionProvider`) and can be seeded with // [customSessionId]. Exposed through [sessionManager] for Session Replay. private val ldSessionManager = LDSessionManager( initialSessionId = customSessionId, @@ -114,7 +114,7 @@ class ObservabilityService( maxLifetime = 4.hours, ) - var sessionManager: SessionManager? = ldSessionManager + var sessionManager: SessionProvider? = ldSessionManager private set private var otelMeter: Meter private var otelLogger: Logger @@ -243,7 +243,7 @@ class ObservabilityService( } val otelRumConfig = createOtelRumConfig() - val rumBuilder = OpenTelemetryRum.builder(application, otelRumConfig) + val rumBuilder = RumBuilder.builder(application, otelRumConfig) .addLoggerProviderCustomizer { sdkLoggerProviderBuilder, _ -> return@addLoggerProviderCustomizer configureLoggerProvider(sdkLoggerProviderBuilder) } @@ -254,9 +254,9 @@ class ObservabilityService( return@addMeterProviderCustomizer configureMeterProvider(sdkMeterProviderBuilder) } - // Use our own session manager (instead of the RUM SDK's default) so we can seed the session + // Use our own session provider (instead of the RUM SDK's default) so we can seed the session // id and keep a single source of session identity across spans, logs, metrics, and replay. - LDRumSessionManagerAccessor.setSessionManager(rumBuilder, ldSessionManager) + rumBuilder.setSessionProvider(ldSessionManager) if (observabilityOptions.enabled && observabilityOptions.instrumentations.launchTime) { addLaunchTimeInstrumentation(rumBuilder) @@ -269,12 +269,12 @@ class ObservabilityService( // previous_screen against the prior session, and a re-appearing first screen would be // deduped instead of emitting a fresh navigation. // - // Only reset on an actual session *change*. The initial session start carries - // Session.NONE (empty id) as the previous session; resetting on it would clobber a first + // Only reset on an actual session *change*. The initial session start carries the "none" + // session (empty id) as the previous session; resetting on it would clobber a first // screen that may already have been recorded by the time this notification fires. - sessionManager?.addObserver(object : SessionObserver { + ldSessionManager.addObserver(object : SessionObserver { override fun onSessionStarted(newSession: Session, previousSession: Session) { - if (previousSession.getId().isNotEmpty()) { + if (previousSession.id.isNotEmpty()) { screenStack.reset() // Re-seed the new session with the screen the user is still viewing. No // onActivityResumed fires for an already-resumed activity, so without this the @@ -458,7 +458,7 @@ class ObservabilityService( private fun createOtelRumConfig(): OtelRumConfig { // Session lifetime/rotation is owned by [ldSessionManager] (injected via - // [LDRumSessionManagerAccessor]), so no SessionConfig is applied here. + // `OpenTelemetryRumBuilder.setSessionProvider`), so no SessionConfig is applied here. val config = OtelRumConfig() if (!observabilityOptions.enabled || !observabilityOptions.instrumentations.crashReporting) { diff --git a/sdk/@launchdarkly/observability-android/lib/src/main/kotlin/com/launchdarkly/observability/otlp/json/OtlpJsonEncoding.kt b/sdk/@launchdarkly/observability-android/lib/src/main/kotlin/com/launchdarkly/observability/otlp/json/OtlpJsonEncoding.kt index 0ad96510a8..d7ece34fb4 100644 --- a/sdk/@launchdarkly/observability-android/lib/src/main/kotlin/com/launchdarkly/observability/otlp/json/OtlpJsonEncoding.kt +++ b/sdk/@launchdarkly/observability-android/lib/src/main/kotlin/com/launchdarkly/observability/otlp/json/OtlpJsonEncoding.kt @@ -112,6 +112,7 @@ private fun JsonByteWriter.write(value: OtlpJsonAnyValue) { endObject() } is OtlpJsonAnyValue.BytesVal -> name("bytesValue").value(value.value) + is OtlpJsonAnyValue.EmptyVal -> Unit } endObject() } diff --git a/sdk/@launchdarkly/observability-android/lib/src/main/kotlin/com/launchdarkly/observability/otlp/json/common/JsonCommonAdapter.kt b/sdk/@launchdarkly/observability-android/lib/src/main/kotlin/com/launchdarkly/observability/otlp/json/common/JsonCommonAdapter.kt index 4e4db6e31c..bf790574fc 100644 --- a/sdk/@launchdarkly/observability-android/lib/src/main/kotlin/com/launchdarkly/observability/otlp/json/common/JsonCommonAdapter.kt +++ b/sdk/@launchdarkly/observability-android/lib/src/main/kotlin/com/launchdarkly/observability/otlp/json/common/JsonCommonAdapter.kt @@ -68,6 +68,8 @@ object JsonCommonAdapter { @Suppress("UNCHECKED_CAST") (value as List).map { OtlpJsonAnyValue.double(it) } ) + // Extended attributes carry a nested Value, which the Value overload already handles. + AttributeType.VALUE -> toJsonAnyValue(value as Value<*>) } /** @@ -99,5 +101,6 @@ object JsonCommonAdapter { buffer.asReadOnlyBuffer().get(bytes) OtlpJsonAnyValue.bytes(Base64.getEncoder().encodeToString(bytes)) } + ValueType.EMPTY -> OtlpJsonAnyValue.empty() } } diff --git a/sdk/@launchdarkly/observability-android/lib/src/main/kotlin/com/launchdarkly/observability/otlp/json/common/OtlpJsonCommonModels.kt b/sdk/@launchdarkly/observability-android/lib/src/main/kotlin/com/launchdarkly/observability/otlp/json/common/OtlpJsonCommonModels.kt index 300f407c78..c6f30a3bb8 100644 --- a/sdk/@launchdarkly/observability-android/lib/src/main/kotlin/com/launchdarkly/observability/otlp/json/common/OtlpJsonCommonModels.kt +++ b/sdk/@launchdarkly/observability-android/lib/src/main/kotlin/com/launchdarkly/observability/otlp/json/common/OtlpJsonCommonModels.kt @@ -59,6 +59,9 @@ sealed class OtlpJsonAnyValue { data class BytesVal(val value: String) : OtlpJsonAnyValue() + /** An unset value, written as `{}` (proto `AnyValue` with no `oneof` member set). */ + object EmptyVal : OtlpJsonAnyValue() + data class ArrayWrapper(val values: List = emptyList()) data class KvListWrapper(val values: List = emptyList()) @@ -71,5 +74,6 @@ sealed class OtlpJsonAnyValue { fun array(values: List): OtlpJsonAnyValue = ArrayVal(ArrayWrapper(values)) fun kvlist(values: List): OtlpJsonAnyValue = KvListVal(KvListWrapper(values)) fun bytes(base64: String): OtlpJsonAnyValue = BytesVal(base64) + fun empty(): OtlpJsonAnyValue = EmptyVal } } diff --git a/sdk/@launchdarkly/observability-android/lib/src/main/kotlin/com/launchdarkly/observability/replay/InteractionMoveGrouper.kt b/sdk/@launchdarkly/observability-android/lib/src/main/kotlin/com/launchdarkly/observability/replay/InteractionMoveGrouper.kt index 7a8851d998..746c96d088 100644 --- a/sdk/@launchdarkly/observability-android/lib/src/main/kotlin/com/launchdarkly/observability/replay/InteractionMoveGrouper.kt +++ b/sdk/@launchdarkly/observability-android/lib/src/main/kotlin/com/launchdarkly/observability/replay/InteractionMoveGrouper.kt @@ -1,7 +1,7 @@ package com.launchdarkly.observability.replay import android.view.MotionEvent -import io.opentelemetry.android.session.SessionManager +import io.opentelemetry.android.session.SessionProvider import kotlinx.coroutines.flow.MutableSharedFlow private const val FILTER_THRESHOLD_DISTANCE_SQUARED_PIXELS = 144 // 12 X 12 pixels, matches iOS tapMaxDistanceSquared @@ -18,7 +18,7 @@ private const val EMIT_PERIOD_MILLIS = 240 // time to gather 4 positions into gr * avoid delays handing off data to collector of flow */ class InteractionMoveGrouper( - private val _sessionManager: SessionManager, + private val _sessionManager: SessionProvider, private val _bufferedFlow: MutableSharedFlow, ) { private val acceptedPositions = mutableListOf() diff --git a/sdk/@launchdarkly/observability-android/lib/src/main/kotlin/com/launchdarkly/observability/replay/InteractionSource.kt b/sdk/@launchdarkly/observability-android/lib/src/main/kotlin/com/launchdarkly/observability/replay/InteractionSource.kt index dcf1ad0e69..5eb53e2160 100644 --- a/sdk/@launchdarkly/observability-android/lib/src/main/kotlin/com/launchdarkly/observability/replay/InteractionSource.kt +++ b/sdk/@launchdarkly/observability-android/lib/src/main/kotlin/com/launchdarkly/observability/replay/InteractionSource.kt @@ -2,7 +2,7 @@ package com.launchdarkly.observability.replay import android.view.MotionEvent import com.launchdarkly.observability.client.TouchSample -import io.opentelemetry.android.session.SessionManager +import io.opentelemetry.android.session.SessionProvider import kotlinx.coroutines.channels.BufferOverflow import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.SharedFlow @@ -23,7 +23,7 @@ import kotlinx.coroutines.flow.asSharedFlow * @param density the display density used to derive the pixel scale factor. */ class InteractionSource( - private val sessionManager: SessionManager, + private val sessionManager: SessionProvider, private val scale: Double?, private val density: Double, ) { diff --git a/sdk/@launchdarkly/observability-android/lib/src/main/kotlin/com/launchdarkly/observability/replay/SessionReplayService.kt b/sdk/@launchdarkly/observability-android/lib/src/main/kotlin/com/launchdarkly/observability/replay/SessionReplayService.kt index a9239e4323..7658a8eae7 100644 --- a/sdk/@launchdarkly/observability-android/lib/src/main/kotlin/com/launchdarkly/observability/replay/SessionReplayService.kt +++ b/sdk/@launchdarkly/observability-android/lib/src/main/kotlin/com/launchdarkly/observability/replay/SessionReplayService.kt @@ -28,7 +28,7 @@ import com.launchdarkly.observability.replay.transport.EventQueue import com.launchdarkly.observability.context.LDObserveContext import com.launchdarkly.observability.sdk.SessionReplayServicing import com.launchdarkly.observability.util.requireMainThread -import io.opentelemetry.android.session.SessionManager +import io.opentelemetry.android.session.SessionProvider import io.opentelemetry.api.common.Attributes import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Job @@ -77,7 +77,7 @@ class SessionReplayService( private val imageCaptureService: ImageCaptureServicing? = null, ) : SessionReplayServicing { - private lateinit var sessionManager: SessionManager + private lateinit var sessionManager: SessionProvider private val logger: ObserveLogger = observabilityContext.logger private val eventQueue = EventQueue() private val batchWorker = BatchWorker(eventQueue, logger) diff --git a/sdk/@launchdarkly/observability-android/lib/src/main/kotlin/com/launchdarkly/observability/replay/capture/CaptureManager.kt b/sdk/@launchdarkly/observability-android/lib/src/main/kotlin/com/launchdarkly/observability/replay/capture/CaptureManager.kt index 9b7b423bab..2e20f7a6c2 100644 --- a/sdk/@launchdarkly/observability-android/lib/src/main/kotlin/com/launchdarkly/observability/replay/capture/CaptureManager.kt +++ b/sdk/@launchdarkly/observability-android/lib/src/main/kotlin/com/launchdarkly/observability/replay/capture/CaptureManager.kt @@ -2,7 +2,7 @@ package com.launchdarkly.observability.replay.capture import com.launchdarkly.observability.context.ObserveLogger import com.launchdarkly.observability.replay.ReplayOptions -import io.opentelemetry.android.session.SessionManager +import io.opentelemetry.android.session.SessionProvider import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.SharedFlow import kotlinx.coroutines.flow.asSharedFlow @@ -14,7 +14,7 @@ import kotlinx.coroutines.flow.asSharedFlow * @param sessionManager Used to get current session for tagging [ExportFrame] with session id */ class CaptureManager( - private val sessionManager: SessionManager, + private val sessionManager: SessionProvider, private val options: ReplayOptions, private val logger: ObserveLogger, private val imageCaptureService: ImageCaptureServicing = ImageCaptureService(options, logger), diff --git a/sdk/@launchdarkly/observability-android/lib/src/main/kotlin/io/opentelemetry/android/LDRumSessionManagerAccessor.kt b/sdk/@launchdarkly/observability-android/lib/src/main/kotlin/io/opentelemetry/android/LDRumSessionManagerAccessor.kt deleted file mode 100644 index 3ba2a22081..0000000000 --- a/sdk/@launchdarkly/observability-android/lib/src/main/kotlin/io/opentelemetry/android/LDRumSessionManagerAccessor.kt +++ /dev/null @@ -1,17 +0,0 @@ -package io.opentelemetry.android - -import io.opentelemetry.android.session.SessionManager - -/** - * Bridges to [OpenTelemetryRumBuilder.setSessionManager], which is package-private in - * OpenTelemetry Android. Declaring this object in the same `io.opentelemetry.android` package lets - * the LaunchDarkly SDK supply its own [SessionManager] (see - * [com.launchdarkly.observability.client.LDSessionManager]) so it backs the RUM SDK's `session.id` - * span/log appenders, rather than relying on the default manager captured via an instrumentation. - */ -internal object LDRumSessionManagerAccessor { - fun setSessionManager( - builder: OpenTelemetryRumBuilder, - sessionManager: SessionManager, - ): OpenTelemetryRumBuilder = builder.setSessionManager(sessionManager) -} diff --git a/sdk/@launchdarkly/observability-android/lib/src/test/kotlin/com/launchdarkly/observability/replay/InteractionMoveGrouperTest.kt b/sdk/@launchdarkly/observability-android/lib/src/test/kotlin/com/launchdarkly/observability/replay/InteractionMoveGrouperTest.kt index b8dc15f725..1397a78f55 100644 --- a/sdk/@launchdarkly/observability-android/lib/src/test/kotlin/com/launchdarkly/observability/replay/InteractionMoveGrouperTest.kt +++ b/sdk/@launchdarkly/observability-android/lib/src/test/kotlin/com/launchdarkly/observability/replay/InteractionMoveGrouperTest.kt @@ -3,7 +3,7 @@ package com.launchdarkly.observability.replay import android.view.MotionEvent import io.mockk.every import io.mockk.mockk -import io.opentelemetry.android.session.SessionManager +import io.opentelemetry.android.session.SessionProvider import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.take import kotlinx.coroutines.flow.toList @@ -14,13 +14,13 @@ import org.junit.jupiter.api.Test class InteractionMoveGrouperTest { - private lateinit var mockSessionManager: SessionManager + private lateinit var mockSessionManager: SessionProvider private lateinit var bufferedFlow: MutableSharedFlow private lateinit var grouper: InteractionMoveGrouper @BeforeEach fun setUp() { - mockSessionManager = mockk(relaxed = true) + mockSessionManager = mockk(relaxed = true) every { mockSessionManager.getSessionId() } returns "test-session-id" bufferedFlow = MutableSharedFlow(replay = 64, extraBufferCapacity = 64) grouper = InteractionMoveGrouper(mockSessionManager, bufferedFlow) diff --git a/sdk/@launchdarkly/observability-android/lib/src/test/kotlin/com/launchdarkly/observability/replay/SessionReplayTest.kt b/sdk/@launchdarkly/observability-android/lib/src/test/kotlin/com/launchdarkly/observability/replay/SessionReplayTest.kt index 874dabcd20..cb6a061f4d 100644 --- a/sdk/@launchdarkly/observability-android/lib/src/test/kotlin/com/launchdarkly/observability/replay/SessionReplayTest.kt +++ b/sdk/@launchdarkly/observability-android/lib/src/test/kotlin/com/launchdarkly/observability/replay/SessionReplayTest.kt @@ -57,7 +57,7 @@ class SessionReplayTest { @Test fun `initialize wires up LDReplay when service install succeeds`() { // Substitute a stub service for the one register() created so we can decide the - // SessionReplayService.initialize() outcome without standing up a real SessionManager, + // SessionReplayService.initialize() outcome without standing up a real SessionProvider, // ProcessLifecycleOwner, etc. — none of which are available in plain JVM tests. val service = mockk(relaxed = true) every { service.initialize() } returns true diff --git a/sdk/@launchdarkly/observability-android/lib/src/test/kotlin/com/launchdarkly/observability/replay/capture/CaptureManagerTest.kt b/sdk/@launchdarkly/observability-android/lib/src/test/kotlin/com/launchdarkly/observability/replay/capture/CaptureManagerTest.kt index c7ab7323af..ba4ad793f6 100644 --- a/sdk/@launchdarkly/observability-android/lib/src/test/kotlin/com/launchdarkly/observability/replay/capture/CaptureManagerTest.kt +++ b/sdk/@launchdarkly/observability-android/lib/src/test/kotlin/com/launchdarkly/observability/replay/capture/CaptureManagerTest.kt @@ -3,7 +3,7 @@ package com.launchdarkly.observability.replay.capture import com.launchdarkly.observability.context.ObserveLogger import com.launchdarkly.observability.replay.ReplayOptions import io.mockk.mockk -import io.opentelemetry.android.session.SessionManager +import io.opentelemetry.android.session.SessionProvider import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Test @@ -24,7 +24,7 @@ class CaptureManagerTest { } private fun captureManager(options: ReplayOptions = ReplayOptions()) = CaptureManager( - sessionManager = mockk(relaxed = true), + sessionManager = mockk(relaxed = true), options = options, logger = mockk(relaxed = true), imageCaptureService = mockk(relaxed = true), From 8d4f125a28626762358f8568e68113914de89312 Mon Sep 17 00:00:00 2001 From: Andrey Belonogov Date: Fri, 2 Oct 2026 16:59:15 -0700 Subject: [PATCH 2/9] feat(maui): add .NET 10 sample with Firebase compatibility check - Add MauiSample10.csproj (net10.0-android/ios) sharing sources with MauiSample9, with isolated obj/bin paths. - Reference Xamarin.Firebase.Messaging/Analytics on Android as a standing check against the Square.OkIO / KotlinX duplicate-class conflicts (exercised by Release builds). - Guard CommunityToolkit.Maui Popup API differences (v11 vs v15) in DialogsPage. - Show platform, .NET runtime version and Debug/Release in the shell title. - Remove global.json so the .NET 10 SDK can build the tree. Co-authored-by: Cursor --- sdk/@launchdarkly/mobile-dotnet/global.json | 6 - .../mobile-dotnet/sample/AppShell.xaml | 2 +- .../mobile-dotnet/sample/AppShell.xaml.cs | 15 ++ .../mobile-dotnet/sample/DialogsPage.xaml.cs | 14 ++ .../mobile-dotnet/sample/MauiSample10.csproj | 149 ++++++++++++++++++ 5 files changed, 179 insertions(+), 7 deletions(-) delete mode 100644 sdk/@launchdarkly/mobile-dotnet/global.json create mode 100644 sdk/@launchdarkly/mobile-dotnet/sample/MauiSample10.csproj diff --git a/sdk/@launchdarkly/mobile-dotnet/global.json b/sdk/@launchdarkly/mobile-dotnet/global.json deleted file mode 100644 index ce8c0c4668..0000000000 --- a/sdk/@launchdarkly/mobile-dotnet/global.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "sdk": { - "version": "9.0.312", - "rollForward": "latestPatch" - } -} 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..8dd8068bd5 --- /dev/null +++ b/sdk/@launchdarkly/mobile-dotnet/sample/MauiSample10.csproj @@ -0,0 +1,149 @@ + + + + + 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 + $(NoWarn);NU1605;NU1608 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + true + + + + true + + false + + + + + From c7e535092e9a7de4c6f0b520807cd9d145661970 Mon Sep 17 00:00:00 2001 From: Andrey Belonogov Date: Fri, 2 Oct 2026 18:10:41 -0700 Subject: [PATCH 3/9] fix(maui): restore AndroidX Lifecycle 2.9.2.1 floors to fix NU1605 in consumer apps 0.11.1 raised Xamarin.AndroidX.Lifecycle.* floors to 2.11.0.1, which requires SavedState >= 1.5.0.1 and Collection.Jvm >= 1.6.0.1 and conflicts with our SavedState 1.3.1.1 / Collection 1.5.0.3 floors, producing NU1605 downgrade errors in MAUI apps (e.g. MauiPlanets on MAUI 9). Our Java only uses lifecycle APIs present in 2.9 (ProcessLifecycleOwner, DefaultLifecycleObserver), so go back to the 2.9.2.1 line MAUI itself ships. Stop suppressing NU1605/NU1608 in MauiSample10 so the sample catches this, and bump the package version to 0.11.2. Co-authored-by: Cursor --- .../LDObserve.Android.Binding.csproj | 18 +++++++-------- .../observability/Directory.Build.props | 2 +- .../observability/LDObservability.Fat.csproj | 23 +++++++++++-------- .../mobile-dotnet/sample/MauiSample10.csproj | 1 - 4 files changed, 24 insertions(+), 20 deletions(-) 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 e791c99c4e..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 @@ -38,14 +38,14 @@ - - - - - - - - - + + + + + + + + + diff --git a/sdk/@launchdarkly/mobile-dotnet/observability/Directory.Build.props b/sdk/@launchdarkly/mobile-dotnet/observability/Directory.Build.props index 96b1bef28a..18ee46a432 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.0 + 0.11.2 LaunchDarkly LaunchDarkly LaunchDarkly diff --git a/sdk/@launchdarkly/mobile-dotnet/observability/LDObservability.Fat.csproj b/sdk/@launchdarkly/mobile-dotnet/observability/LDObservability.Fat.csproj index ca454df567..359013ccd0 100644 --- a/sdk/@launchdarkly/mobile-dotnet/observability/LDObservability.Fat.csproj +++ b/sdk/@launchdarkly/mobile-dotnet/observability/LDObservability.Fat.csproj @@ -28,17 +28,22 @@ - - - + + + - - - + + + - - - + + + diff --git a/sdk/@launchdarkly/mobile-dotnet/sample/MauiSample10.csproj b/sdk/@launchdarkly/mobile-dotnet/sample/MauiSample10.csproj index 8dd8068bd5..fe96fd899c 100644 --- a/sdk/@launchdarkly/mobile-dotnet/sample/MauiSample10.csproj +++ b/sdk/@launchdarkly/mobile-dotnet/sample/MauiSample10.csproj @@ -53,7 +53,6 @@ 10.0.17763.0 10.0.17763.0 6.5 - $(NoWarn);NU1605;NU1608 From 16ecdb4b99ba52accee27b0dcc591d497a0ece39 Mon Sep 17 00:00:00 2001 From: Andrey Belonogov Date: Mon, 5 Oct 2026 19:31:03 -0700 Subject: [PATCH 4/9] docs(maui): align README with current API and supported platforms Co-authored-by: Cursor --- sdk/@launchdarkly/mobile-dotnet/README.md | 29 ++++++++++++++++++----- 1 file changed, 23 insertions(+), 6 deletions(-) diff --git a/sdk/@launchdarkly/mobile-dotnet/README.md b/sdk/@launchdarkly/mobile-dotnet/README.md index d24be4e430..42e492c553 100644 --- a/sdk/@launchdarkly/mobile-dotnet/README.md +++ b/sdk/@launchdarkly/mobile-dotnet/README.md @@ -11,19 +11,21 @@ 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`) +- **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. ## 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 +66,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 +179,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 From 301f6e76775b9c6e98f06bd949bd48d5a010a7a6 Mon Sep 17 00:00:00 2001 From: Andrey Belonogov Date: Tue, 6 Oct 2026 08:36:27 -0700 Subject: [PATCH 5/9] revert(maui): hold OTel at 1.51 / 0.11.0-alpha The okio/okhttp relocation is what fixes the R8 duplicate-class failure in Release, and it is independent of the OTel version: 0.10.2, which shipped on OTel 1.51, already bundles 433 unshaded okio/okhttp3 entries including the okio/-DeflaterSinkExtensions that R8 rejects. Upgrading to OTel Android 1.7 pulls androidx.core 1.19, androidx.lifecycle 2.11 and okhttp-android 5.4, which require compileSdk 36-37 and AGP 9.1. That breaks every native Android consumer still on AGP 8, including our own e2e app, and is a migration the Android owners should land on its own schedule. Reverts the version bumps and the observability-android changes that existed only to track the 1.x API. Keeps the relocation, the Lifecycle floors, the .NET 10 sample and the Analytics.screenViews fix. Co-authored-by: Cursor --- .../android/native/LDObserve/build.gradle.kts | 81 ++++++------------- .../observability/LDObservability.Fat.csproj | 9 +-- .../observability/NativeAndroidDeps.props | 25 +----- .../lib/build.gradle.kts | 47 +++-------- .../observability/bridge/LDValueAttributes.kt | 1 - .../observability/client/LDSessionManager.kt | 57 ++++--------- .../client/LaunchTimeInstrumentation.kt | 4 +- .../client/ObservabilityContext.kt | 4 +- .../client/ObservabilityService.kt | 24 +++--- .../otlp/json/OtlpJsonEncoding.kt | 1 - .../otlp/json/common/JsonCommonAdapter.kt | 3 - .../otlp/json/common/OtlpJsonCommonModels.kt | 4 - .../replay/InteractionMoveGrouper.kt | 4 +- .../observability/replay/InteractionSource.kt | 4 +- .../replay/SessionReplayService.kt | 4 +- .../replay/capture/CaptureManager.kt | 4 +- .../android/LDRumSessionManagerAccessor.kt | 17 ++++ .../replay/InteractionMoveGrouperTest.kt | 6 +- .../observability/replay/SessionReplayTest.kt | 2 +- .../replay/capture/CaptureManagerTest.kt | 4 +- 20 files changed, 106 insertions(+), 199 deletions(-) create mode 100644 sdk/@launchdarkly/observability-android/lib/src/main/kotlin/io/opentelemetry/android/LDRumSessionManagerAccessor.kt diff --git a/sdk/@launchdarkly/mobile-dotnet/android/native/LDObserve/build.gradle.kts b/sdk/@launchdarkly/mobile-dotnet/android/native/LDObserve/build.gradle.kts index 9c97665f2a..6a28584707 100644 --- a/sdk/@launchdarkly/mobile-dotnet/android/native/LDObserve/build.gradle.kts +++ b/sdk/@launchdarkly/mobile-dotnet/android/native/LDObserve/build.gradle.kts @@ -55,32 +55,6 @@ configurations { create("copyDependencies") } -// Mirrors the pins in observability-android: OTel Android 1.7 drags in kotlinx-coroutines 1.11.0 -// (Kotlin 2.2 metadata, unreadable by the 2.0.21 compiler) and androidx.core 1.19.0 (needs AGP 9.1 -// / compileSdk 37). Both are held back to the newest releases this toolchain can build against. -configurations.all { - resolutionStrategy.eachDependency { - val name = requested.name - // OTel Android 1.7 depends on kotlin-stdlib 2.4.10, whose metadata this module's 2.0.21 - // compiler cannot read. Hold the Kotlin runtime at the compiler's own version. - val isKotlinRuntime = name.startsWith("kotlin-stdlib") || - name == "kotlin-reflect" || - name.startsWith("kotlin-test") - if (requested.group == "org.jetbrains.kotlin" && isKotlinRuntime) { - useVersion("2.0.21") - } - if (requested.group == "org.jetbrains.kotlinx" && name.startsWith("kotlinx-coroutines")) { - useVersion("1.10.2") - } - if (requested.group == "androidx.core" && (name == "core" || name == "core-ktx")) { - useVersion("1.16.0") - } - if (requested.group == "com.squareup.okhttp3") { - useVersion("5.2.1") - } - } -} - dependencies { implementation("androidx.core:core-ktx:1.15.0") "copyDependencies"("androidx.core:core-ktx:1.15.0") @@ -93,52 +67,45 @@ dependencies { // TODO: revise these versions to be as old as usable for compatibility // OpenTelemetry JARs copied here are filtered for NuGet in observability/LDObservability.Fat.csproj (autoconfigure vs autoconfigure-spi). - implementation("io.opentelemetry:opentelemetry-api:1.66.0") - "copyDependencies"("io.opentelemetry:opentelemetry-api:1.66.0") + implementation("io.opentelemetry:opentelemetry-api:1.51.0") + "copyDependencies"("io.opentelemetry:opentelemetry-api:1.51.0") - implementation("io.opentelemetry:opentelemetry-sdk:1.66.0") - "copyDependencies"("io.opentelemetry:opentelemetry-sdk:1.66.0") + implementation("io.opentelemetry:opentelemetry-sdk:1.51.0") + "copyDependencies"("io.opentelemetry:opentelemetry-sdk:1.51.0") - implementation("io.opentelemetry:opentelemetry-exporter-otlp:1.66.0") - "copyDependencies"("io.opentelemetry:opentelemetry-exporter-otlp:1.66.0") + implementation("io.opentelemetry:opentelemetry-exporter-otlp:1.51.0") + "copyDependencies"("io.opentelemetry:opentelemetry-exporter-otlp:1.51.0") - implementation("io.opentelemetry:opentelemetry-exporter-sender-okhttp:1.66.0") - "copyDependencies"("io.opentelemetry:opentelemetry-exporter-sender-okhttp:1.66.0") + implementation("io.opentelemetry:opentelemetry-exporter-sender-okhttp:1.51.0") + "copyDependencies"("io.opentelemetry:opentelemetry-exporter-sender-okhttp:1.51.0") - implementation("io.opentelemetry:opentelemetry-exporter-logging-otlp:1.66.0") - "copyDependencies"("io.opentelemetry:opentelemetry-exporter-logging-otlp:1.66.0") + implementation("io.opentelemetry:opentelemetry-exporter-logging-otlp:1.51.0") + "copyDependencies"("io.opentelemetry:opentelemetry-exporter-logging-otlp:1.51.0") - implementation("io.opentelemetry:opentelemetry-sdk-metrics:1.66.0") - "copyDependencies"("io.opentelemetry:opentelemetry-sdk-metrics:1.66.0") + implementation("io.opentelemetry:opentelemetry-sdk-metrics:1.51.0") + "copyDependencies"("io.opentelemetry:opentelemetry-sdk-metrics:1.51.0") - implementation("io.opentelemetry:opentelemetry-sdk-logs:1.66.0") - "copyDependencies"("io.opentelemetry:opentelemetry-sdk-logs:1.66.0") + implementation("io.opentelemetry:opentelemetry-sdk-logs:1.51.0") + "copyDependencies"("io.opentelemetry:opentelemetry-sdk-logs:1.51.0") // TODO: Evaluate risks associated with incubator APIs - implementation("io.opentelemetry:opentelemetry-api-incubator:1.66.0-alpha") - "copyDependencies"("io.opentelemetry:opentelemetry-api-incubator:1.66.0-alpha") + implementation("io.opentelemetry:opentelemetry-api-incubator:1.51.0-alpha") + "copyDependencies"("io.opentelemetry:opentelemetry-api-incubator:1.51.0-alpha") // OTEL Android - implementation("io.opentelemetry.android:core:1.7.0-alpha") - "copyDependencies"("io.opentelemetry.android:core:1.7.0-alpha") - - implementation("io.opentelemetry.android:session:1.7.0") - "copyDependencies"("io.opentelemetry.android:session:1.7.0") - - // `OpenTelemetryRum` and `AndroidInstrumentation` moved out of :core in 1.x. - implementation("io.opentelemetry.android:agent-api:1.7.0") - "copyDependencies"("io.opentelemetry.android:agent-api:1.7.0") + implementation("io.opentelemetry.android:core:0.11.0-alpha") + "copyDependencies"("io.opentelemetry.android:core:0.11.0-alpha") - implementation("io.opentelemetry.android.instrumentation:android-instrumentation:1.7.0") - "copyDependencies"("io.opentelemetry.android.instrumentation:android-instrumentation:1.7.0") + implementation("io.opentelemetry.android:session:0.11.0-alpha") + "copyDependencies"("io.opentelemetry.android:session:0.11.0-alpha") // OTEL Android Instrumentations - implementation("io.opentelemetry.android.instrumentation:crash:1.7.0-alpha") - "copyDependencies"("io.opentelemetry.android.instrumentation:crash:1.7.0-alpha") + implementation("io.opentelemetry.android.instrumentation:crash:0.11.0-alpha") + "copyDependencies"("io.opentelemetry.android.instrumentation:crash:0.11.0-alpha") - implementation("io.opentelemetry.android:android-agent:1.7.0") - "copyDependencies"("io.opentelemetry.android:android-agent:1.7.0") + implementation("io.opentelemetry.android:android-agent:0.11.0-alpha") + "copyDependencies"("io.opentelemetry.android:android-agent:0.11.0-alpha") } // Custom task: merges all transitive JARs (OTel + okhttp + okio + ...) into a diff --git a/sdk/@launchdarkly/mobile-dotnet/observability/LDObservability.Fat.csproj b/sdk/@launchdarkly/mobile-dotnet/observability/LDObservability.Fat.csproj index 359013ccd0..8675fa53b1 100644 --- a/sdk/@launchdarkly/mobile-dotnet/observability/LDObservability.Fat.csproj +++ b/sdk/@launchdarkly/mobile-dotnet/observability/LDObservability.Fat.csproj @@ -28,11 +28,10 @@ - diff --git a/sdk/@launchdarkly/mobile-dotnet/observability/NativeAndroidDeps.props b/sdk/@launchdarkly/mobile-dotnet/observability/NativeAndroidDeps.props index a77a25c5dd..325b34f8a5 100644 --- a/sdk/@launchdarkly/mobile-dotnet/observability/NativeAndroidDeps.props +++ b/sdk/@launchdarkly/mobile-dotnet/observability/NativeAndroidDeps.props @@ -18,39 +18,20 @@ - - <_LDNativeAAR Include="$(_LDNativeDepsDir)agent-api-*.aar" /> + <_LDNativeAAR Include="$(_LDNativeDepsDir)android-agent-*.aar" /> <_LDNativeAAR Include="$(_LDNativeDepsDir)android-instrumentation-*.aar" /> - - <_LDNativeAAR Include="$(_LDNativeDepsDir)common-1*.aar" /> + <_LDNativeAAR Include="$(_LDNativeDepsDir)common-0*.aar" /> <_LDNativeAAR Include="$(_LDNativeDepsDir)common-api-*.aar" /> <_LDNativeAAR Include="$(_LDNativeDepsDir)core-*-alpha.aar" /> <_LDNativeAAR Include="$(_LDNativeDepsDir)anr-*-alpha.aar" /> <_LDNativeAAR Include="$(_LDNativeDepsDir)activity-*-alpha.aar" /> <_LDNativeAAR Include="$(_LDNativeDepsDir)fragment-*-alpha.aar" /> <_LDNativeAAR Include="$(_LDNativeDepsDir)network-*-alpha.aar" /> - <_LDNativeAAR Include="$(_LDNativeDepsDir)screen-orientation-*-alpha.aar" /> <_LDNativeAAR Include="$(_LDNativeDepsDir)services-*-alpha.aar" /> - - <_LDNativeAAR Include="$(_LDNativeDepsDir)session-*.aar" /> - <_LDNativeAAR Include="$(_LDNativeDepsDir)sessions-*-alpha.aar" /> + <_LDNativeAAR Include="$(_LDNativeDepsDir)session-*-alpha.aar" /> <_LDNativeAAR Include="$(_LDNativeDepsDir)slowrendering-*-alpha.aar" /> <_LDNativeAAR Include="$(_LDNativeDepsDir)startup-*-alpha.aar" /> - - <_LDNativeAAR Include="$(_LDNativeDepsDir)semconv-*-alpha.aar" /> - - <_LDNativeAAR Include="$(_LDNativeDepsDir)semconv.aar" /> <_LDNativeAAR Include="$(_LDNativeDepsDir)lib-release.aar" /> diff --git a/sdk/@launchdarkly/observability-android/lib/build.gradle.kts b/sdk/@launchdarkly/observability-android/lib/build.gradle.kts index e033acb0a6..7f803fc092 100644 --- a/sdk/@launchdarkly/observability-android/lib/build.gradle.kts +++ b/sdk/@launchdarkly/observability-android/lib/build.gradle.kts @@ -31,7 +31,7 @@ val isClientSdkProvidedByHost = // Pin Kotlin runtime artifacts on this build's classpath to match the configured Kotlin // compiler version (2.0.21). Without this, transitive deps such as the -// io.opentelemetry.android:*:1.7.0 modules drag in newer kotlin-stdlib / kotlin-reflect +// io.opentelemetry.android:*:0.11.0-alpha modules drag in newer kotlin-stdlib / kotlin-reflect // whose metadata version the 2.0.21 compiler cannot read, producing // "Module was compiled with an incompatible version of Kotlin" errors at compileDebugKotlin. // @@ -50,26 +50,6 @@ configurations.all { useVersion("2.0.21") because("Align Kotlin runtime artifacts with the project's Kotlin compiler version (2.0.21).") } - // io.opentelemetry.android:*:1.7.0 pulls androidx.lifecycle 2.11.0, which in turn requires - // kotlinx-coroutines 1.11.0. That release carries Kotlin metadata 2.2.0, and the 2.0.21 - // compiler reads at most 2.1.0. 1.10.2 is the newest release still on metadata 2.1.0. - if (requested.group == "org.jetbrains.kotlinx" && name.startsWith("kotlinx-coroutines")) { - useVersion("1.10.2") - because("kotlinx-coroutines 1.11.0 carries Kotlin 2.2 metadata the 2.0.21 compiler cannot read.") - } - // io.opentelemetry.android:common:1.7.0-alpha pulls androidx.core 1.19.0, which refuses to - // build on anything below AGP 9.1 / compileSdk 37. Hold it at the newest release that still - // builds against compileSdk 35 with AGP 8.x. - if (requested.group == "androidx.core" && (name == "core" || name == "core-ktx")) { - useVersion("1.16.0") - because("androidx.core 1.17.0+ requires a compileSdk/AGP combination this project does not target yet.") - } - // Same story for okhttp: the OTel 1.66 okhttp sender asks for 5.5.0, whose Android variant - // requires compileSdk 37. - if (requested.group == "com.squareup.okhttp3") { - useVersion("5.2.1") - because("okhttp-android 5.3.0+ requires compileSdk 37, beyond what AGP 8.x supports here.") - } } } @@ -93,33 +73,28 @@ dependencies { implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.9.0") // TODO: revise these versions to be as old as usable for compatibility - implementation("io.opentelemetry:opentelemetry-api:1.66.0") - implementation("io.opentelemetry:opentelemetry-sdk:1.66.0") - implementation("io.opentelemetry:opentelemetry-sdk-metrics:1.66.0") - implementation("io.opentelemetry:opentelemetry-sdk-logs:1.66.0") + implementation("io.opentelemetry:opentelemetry-api:1.51.0") + implementation("io.opentelemetry:opentelemetry-sdk:1.51.0") + implementation("io.opentelemetry:opentelemetry-sdk-metrics:1.51.0") + implementation("io.opentelemetry:opentelemetry-sdk-logs:1.51.0") // Required at runtime by io.opentelemetry.android:core, which uses incubator APIs // internally for the logs bridge. Can be removed once the OTel Android SDK drops this dependency. - implementation("io.opentelemetry:opentelemetry-api-incubator:1.66.0-alpha") + implementation("io.opentelemetry:opentelemetry-api-incubator:1.51.0-alpha") // OTEL Android - implementation("io.opentelemetry.android:core:1.7.0-alpha") - implementation("io.opentelemetry.android:session:1.7.0") - - // `OpenTelemetryRum` and `AndroidInstrumentation` moved out of :core in 1.x and are only - // runtime-scoped transitives of it, so they must be declared to be on the compile classpath. - implementation("io.opentelemetry.android:agent-api:1.7.0") - implementation("io.opentelemetry.android.instrumentation:android-instrumentation:1.7.0") + implementation("io.opentelemetry.android:core:0.11.0-alpha") + implementation("io.opentelemetry.android:session:0.11.0-alpha") // OTEL Android Instrumentations - implementation("io.opentelemetry.android.instrumentation:crash:1.7.0-alpha") + implementation("io.opentelemetry.android.instrumentation:crash:0.11.0-alpha") // NOTE: the `activity` instrumentation is intentionally NOT depended on. It is superseded by // LaunchDarkly's own app/screen lifecycle spans and would otherwise double-report; we also // defensively suppress it by name (see ObservabilityService.createOtelRumConfig). // Use JUnit Jupiter for testing. // Testing exporters for telemetry inspection - testImplementation("io.opentelemetry:opentelemetry-sdk-testing:1.66.0") + testImplementation("io.opentelemetry:opentelemetry-sdk-testing:1.51.0") testImplementation(platform("org.junit:junit-bom:5.13.4")) testImplementation("org.junit.jupiter:junit-jupiter") testRuntimeOnly("org.junit.platform:junit-platform-launcher") @@ -129,7 +104,7 @@ dependencies { testImplementation("io.mockk:mockk:1.14.5") testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.9.0") - testFixturesApi("io.opentelemetry:opentelemetry-sdk-testing:1.66.0") + testFixturesApi("io.opentelemetry:opentelemetry-sdk-testing:1.51.0") testFixturesImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.9.0") } diff --git a/sdk/@launchdarkly/observability-android/lib/src/main/kotlin/com/launchdarkly/observability/bridge/LDValueAttributes.kt b/sdk/@launchdarkly/observability-android/lib/src/main/kotlin/com/launchdarkly/observability/bridge/LDValueAttributes.kt index d5d104038b..a2c3daedd7 100644 --- a/sdk/@launchdarkly/observability-android/lib/src/main/kotlin/com/launchdarkly/observability/bridge/LDValueAttributes.kt +++ b/sdk/@launchdarkly/observability-android/lib/src/main/kotlin/com/launchdarkly/observability/bridge/LDValueAttributes.kt @@ -112,5 +112,4 @@ private fun reKey(original: AttributeKey<*>, newName: String): AttributeKey<*> = AttributeType.BOOLEAN_ARRAY -> AttributeKey.booleanArrayKey(newName) AttributeType.LONG_ARRAY -> AttributeKey.longArrayKey(newName) AttributeType.DOUBLE_ARRAY -> AttributeKey.doubleArrayKey(newName) - AttributeType.VALUE -> AttributeKey.valueKey(newName) } diff --git a/sdk/@launchdarkly/observability-android/lib/src/main/kotlin/com/launchdarkly/observability/client/LDSessionManager.kt b/sdk/@launchdarkly/observability-android/lib/src/main/kotlin/com/launchdarkly/observability/client/LDSessionManager.kt index 7c9df01a64..38a378d862 100644 --- a/sdk/@launchdarkly/observability-android/lib/src/main/kotlin/com/launchdarkly/observability/client/LDSessionManager.kt +++ b/sdk/@launchdarkly/observability-android/lib/src/main/kotlin/com/launchdarkly/observability/client/LDSessionManager.kt @@ -1,29 +1,28 @@ package com.launchdarkly.observability.client import io.opentelemetry.android.session.Session +import io.opentelemetry.android.session.SessionIdGenerator +import io.opentelemetry.android.session.SessionManager import io.opentelemetry.android.session.SessionObserver -import io.opentelemetry.android.session.SessionProvider -import io.opentelemetry.android.session.SessionPublisher import io.opentelemetry.sdk.common.Clock -import java.security.SecureRandom import java.util.Collections.synchronizedList import kotlin.time.Duration /** - * LaunchDarkly's own session source, used in place of OpenTelemetry Android's default so we can + * LaunchDarkly's own [SessionManager], used in place of OpenTelemetry Android's default so we can * **seed the initial session id** ([initialSessionId]). This lets the native observability instance * adopt a session id created elsewhere (e.g. by the JavaScript SDK on the same device), so that * spans, logs, metrics, and Session Replay all report the same `session.id`. * - * Injected into [io.opentelemetry.android.OpenTelemetryRumBuilder] via `setSessionProvider` so it - * backs OpenTelemetry Android's own `session.id` span/log appenders as well — making it the single - * source of session identity. + * Injected into [io.opentelemetry.android.OpenTelemetryRumBuilder] via + * [io.opentelemetry.android.LDRumSessionManagerAccessor] so it backs OpenTelemetry Android's own + * `session.id` span/log appenders as well — making it the single source of session identity. * * When [initialSessionId] is supplied the session is treated as *custom*: the caller owns the * session lifecycle, so automatic rotation is disabled and the seeded id is used for the lifetime * of this manager (mirrors iOS's `isCustomSession`). * - * Otherwise rotation mirrors OpenTelemetry Android's default session manager / + * Otherwise rotation mirrors OpenTelemetry Android's default `SessionManagerImpl` / * `SessionIdTimeoutHandler`: * - a foreground session never times out; * - a background gap of at least [backgroundInactivityTimeout] rotates the session on next use; @@ -38,15 +37,15 @@ import kotlin.time.Duration * @param backgroundInactivityTimeout Background inactivity after which the session rotates. * @param maxLifetime Absolute maximum session lifetime before rotation. * @param clock Time source; defaults to the OpenTelemetry default clock. - * @param idGenerator Generator for new session ids; defaults to a random 128-bit hex id. + * @param idGenerator Generator for new session ids; defaults to OpenTelemetry's. */ internal class LDSessionManager( initialSessionId: String? = null, private val backgroundInactivityTimeout: Duration, private val maxLifetime: Duration, private val clock: Clock = Clock.getDefault(), - private val idGenerator: () -> String = ::randomSessionId, -) : SessionProvider, SessionPublisher { + private val idGenerator: SessionIdGenerator = SessionIdGenerator.DEFAULT, +) : SessionManager { private val lock = Any() private val observers = synchronizedList(ArrayList()) @@ -61,11 +60,11 @@ internal class LDSessionManager( // Guarded by [lock]. private var session: Session = if (isCustomSession) { - LDSession(initialSessionId!!, clock.now()) + Session.DefaultSession(initialSessionId!!, clock.now()) } else { - // Empty id + zero timestamp forces generation on first read, exactly like the default - // manager's initial "none" session. - NONE_SESSION + // Empty id + (-1) timestamp forces generation on first getSessionId(), exactly like + // the default manager's initial Session.NONE. + Session.NONE } // Timeout bookkeeping, mirroring SessionIdTimeoutHandler. @@ -88,7 +87,7 @@ internal class LDSessionManager( var candidate = session // An externally supplied session id is never rotated; the caller owns its lifecycle. if (!isCustomSession && (sessionHasExpired() || hasTimedOut())) { - candidate = LDSession(idGenerator(), clock.now()) + candidate = Session.DefaultSession(idGenerator.generateSessionId(), clock.now()) } // Bump the inactivity timer after deciding, before notifying (a new span may be created). bump() @@ -107,7 +106,7 @@ internal class LDSessionManager( observer.onSessionStarted(newSession, previousSession) } } - return newSession.id + return newSession.getId() } /** Marks the app as transitioning to the foreground; the next event settles it to foreground. */ @@ -121,7 +120,7 @@ internal class LDSessionManager( } private fun sessionHasExpired(): Boolean { - val elapsed = clock.now() - session.startTimestamp + val elapsed = clock.now() - session.getStartTimestamp() return elapsed >= maxLifetime.inWholeNanoseconds } @@ -147,26 +146,4 @@ internal class LDSessionManager( /** Temporary state for the first event after the app is brought back to the foreground. */ TRANSITIONING_TO_FOREGROUND, } - - private class LDSession( - override val id: String, - override val startTimestamp: Long, - ) : Session - - private companion object { - /** - * Stands in for the default manager's "no session yet" value. The zero start timestamp - * makes the first expiry check rotate immediately, generating the real id lazily. - */ - private val NONE_SESSION: Session = LDSession("", 0L) - } -} - -private val sessionIdRandom = SecureRandom() - -/** Generates a random 128-bit session id, rendered as 32 lowercase hex characters. */ -private fun randomSessionId(): String { - val bytes = ByteArray(16) - sessionIdRandom.nextBytes(bytes) - return bytes.joinToString("") { "%02x".format(it) } } diff --git a/sdk/@launchdarkly/observability-android/lib/src/main/kotlin/com/launchdarkly/observability/client/LaunchTimeInstrumentation.kt b/sdk/@launchdarkly/observability-android/lib/src/main/kotlin/com/launchdarkly/observability/client/LaunchTimeInstrumentation.kt index 687fc0e0af..8d0be0a959 100644 --- a/sdk/@launchdarkly/observability-android/lib/src/main/kotlin/com/launchdarkly/observability/client/LaunchTimeInstrumentation.kt +++ b/sdk/@launchdarkly/observability-android/lib/src/main/kotlin/com/launchdarkly/observability/client/LaunchTimeInstrumentation.kt @@ -11,8 +11,8 @@ import android.view.View import android.view.ViewTreeObserver.OnDrawListener import androidx.activity.FullyDrawnReporterOwner import com.launchdarkly.observability.interfaces.Metric -import io.opentelemetry.android.OpenTelemetryRum import io.opentelemetry.android.instrumentation.AndroidInstrumentation +import io.opentelemetry.android.instrumentation.InstallationContext import io.opentelemetry.api.common.Attributes import java.lang.ref.WeakReference import java.util.WeakHashMap @@ -44,7 +44,7 @@ internal class LaunchTimeInstrumentation( override val name: String = INSTRUMENTATION_SCOPE_NAME - override fun install(context: android.content.Context, rum: OpenTelemetryRum) { + override fun install(ctx: InstallationContext) { application.registerActivityLifecycleCallbacks(this) } diff --git a/sdk/@launchdarkly/observability-android/lib/src/main/kotlin/com/launchdarkly/observability/client/ObservabilityContext.kt b/sdk/@launchdarkly/observability-android/lib/src/main/kotlin/com/launchdarkly/observability/client/ObservabilityContext.kt index ae787e4b89..7d2f1a2c4d 100644 --- a/sdk/@launchdarkly/observability-android/lib/src/main/kotlin/com/launchdarkly/observability/client/ObservabilityContext.kt +++ b/sdk/@launchdarkly/observability-android/lib/src/main/kotlin/com/launchdarkly/observability/client/ObservabilityContext.kt @@ -5,7 +5,7 @@ import com.launchdarkly.observability.api.ObservabilityOptions import com.launchdarkly.observability.client.screen.ScreenViewEvent import com.launchdarkly.observability.client.screen.ScreenViewManager import com.launchdarkly.observability.context.ObserveLogger -import io.opentelemetry.android.session.SessionProvider +import io.opentelemetry.android.session.SessionManager import io.opentelemetry.api.common.Attributes import kotlinx.coroutines.flow.SharedFlow @@ -17,7 +17,7 @@ data class ObservabilityContext( val options: ObservabilityOptions, val application: Application, val logger: ObserveLogger, - var sessionManager: SessionProvider? = null, + var sessionManager: SessionManager? = null, var resourceAttributes: Attributes = Attributes.empty(), /** * The single touch-capture hook owned by Observability. Session Replay consumes its diff --git a/sdk/@launchdarkly/observability-android/lib/src/main/kotlin/com/launchdarkly/observability/client/ObservabilityService.kt b/sdk/@launchdarkly/observability-android/lib/src/main/kotlin/com/launchdarkly/observability/client/ObservabilityService.kt index 78e64d3857..fa099f6c45 100644 --- a/sdk/@launchdarkly/observability-android/lib/src/main/kotlin/com/launchdarkly/observability/client/ObservabilityService.kt +++ b/sdk/@launchdarkly/observability-android/lib/src/main/kotlin/com/launchdarkly/observability/client/ObservabilityService.kt @@ -35,13 +35,13 @@ import com.launchdarkly.observability.sampling.SamplingLogProcessor import com.launchdarkly.observability.traces.EventSpanProcessor import com.launchdarkly.observability.traces.OtlpTraceExporter import com.launchdarkly.observability.util.requireMainThread +import io.opentelemetry.android.LDRumSessionManagerAccessor import io.opentelemetry.android.OpenTelemetryRum import io.opentelemetry.android.OpenTelemetryRumBuilder -import io.opentelemetry.android.RumBuilder import io.opentelemetry.android.config.OtelRumConfig import io.opentelemetry.android.session.Session +import io.opentelemetry.android.session.SessionManager import io.opentelemetry.android.session.SessionObserver -import io.opentelemetry.android.session.SessionProvider import io.opentelemetry.api.common.AttributeKey import io.opentelemetry.api.common.Attributes import io.opentelemetry.api.logs.Logger @@ -106,7 +106,7 @@ class ObservabilityService( private val otelRUM: OpenTelemetryRum // LaunchDarkly's own session manager. Owns session identity for all signals (it also backs the - // RUM SDK's `session.id` appenders via `setSessionProvider`) and can be seeded with + // RUM SDK's `session.id` appenders via LDRumSessionManagerAccessor) and can be seeded with // [customSessionId]. Exposed through [sessionManager] for Session Replay. private val ldSessionManager = LDSessionManager( initialSessionId = customSessionId, @@ -114,7 +114,7 @@ class ObservabilityService( maxLifetime = 4.hours, ) - var sessionManager: SessionProvider? = ldSessionManager + var sessionManager: SessionManager? = ldSessionManager private set private var otelMeter: Meter private var otelLogger: Logger @@ -243,7 +243,7 @@ class ObservabilityService( } val otelRumConfig = createOtelRumConfig() - val rumBuilder = RumBuilder.builder(application, otelRumConfig) + val rumBuilder = OpenTelemetryRum.builder(application, otelRumConfig) .addLoggerProviderCustomizer { sdkLoggerProviderBuilder, _ -> return@addLoggerProviderCustomizer configureLoggerProvider(sdkLoggerProviderBuilder) } @@ -254,9 +254,9 @@ class ObservabilityService( return@addMeterProviderCustomizer configureMeterProvider(sdkMeterProviderBuilder) } - // Use our own session provider (instead of the RUM SDK's default) so we can seed the session + // Use our own session manager (instead of the RUM SDK's default) so we can seed the session // id and keep a single source of session identity across spans, logs, metrics, and replay. - rumBuilder.setSessionProvider(ldSessionManager) + LDRumSessionManagerAccessor.setSessionManager(rumBuilder, ldSessionManager) if (observabilityOptions.enabled && observabilityOptions.instrumentations.launchTime) { addLaunchTimeInstrumentation(rumBuilder) @@ -269,12 +269,12 @@ class ObservabilityService( // previous_screen against the prior session, and a re-appearing first screen would be // deduped instead of emitting a fresh navigation. // - // Only reset on an actual session *change*. The initial session start carries the "none" - // session (empty id) as the previous session; resetting on it would clobber a first + // Only reset on an actual session *change*. The initial session start carries + // Session.NONE (empty id) as the previous session; resetting on it would clobber a first // screen that may already have been recorded by the time this notification fires. - ldSessionManager.addObserver(object : SessionObserver { + sessionManager?.addObserver(object : SessionObserver { override fun onSessionStarted(newSession: Session, previousSession: Session) { - if (previousSession.id.isNotEmpty()) { + if (previousSession.getId().isNotEmpty()) { screenStack.reset() // Re-seed the new session with the screen the user is still viewing. No // onActivityResumed fires for an already-resumed activity, so without this the @@ -458,7 +458,7 @@ class ObservabilityService( private fun createOtelRumConfig(): OtelRumConfig { // Session lifetime/rotation is owned by [ldSessionManager] (injected via - // `OpenTelemetryRumBuilder.setSessionProvider`), so no SessionConfig is applied here. + // [LDRumSessionManagerAccessor]), so no SessionConfig is applied here. val config = OtelRumConfig() if (!observabilityOptions.enabled || !observabilityOptions.instrumentations.crashReporting) { diff --git a/sdk/@launchdarkly/observability-android/lib/src/main/kotlin/com/launchdarkly/observability/otlp/json/OtlpJsonEncoding.kt b/sdk/@launchdarkly/observability-android/lib/src/main/kotlin/com/launchdarkly/observability/otlp/json/OtlpJsonEncoding.kt index d7ece34fb4..0ad96510a8 100644 --- a/sdk/@launchdarkly/observability-android/lib/src/main/kotlin/com/launchdarkly/observability/otlp/json/OtlpJsonEncoding.kt +++ b/sdk/@launchdarkly/observability-android/lib/src/main/kotlin/com/launchdarkly/observability/otlp/json/OtlpJsonEncoding.kt @@ -112,7 +112,6 @@ private fun JsonByteWriter.write(value: OtlpJsonAnyValue) { endObject() } is OtlpJsonAnyValue.BytesVal -> name("bytesValue").value(value.value) - is OtlpJsonAnyValue.EmptyVal -> Unit } endObject() } diff --git a/sdk/@launchdarkly/observability-android/lib/src/main/kotlin/com/launchdarkly/observability/otlp/json/common/JsonCommonAdapter.kt b/sdk/@launchdarkly/observability-android/lib/src/main/kotlin/com/launchdarkly/observability/otlp/json/common/JsonCommonAdapter.kt index bf790574fc..4e4db6e31c 100644 --- a/sdk/@launchdarkly/observability-android/lib/src/main/kotlin/com/launchdarkly/observability/otlp/json/common/JsonCommonAdapter.kt +++ b/sdk/@launchdarkly/observability-android/lib/src/main/kotlin/com/launchdarkly/observability/otlp/json/common/JsonCommonAdapter.kt @@ -68,8 +68,6 @@ object JsonCommonAdapter { @Suppress("UNCHECKED_CAST") (value as List).map { OtlpJsonAnyValue.double(it) } ) - // Extended attributes carry a nested Value, which the Value overload already handles. - AttributeType.VALUE -> toJsonAnyValue(value as Value<*>) } /** @@ -101,6 +99,5 @@ object JsonCommonAdapter { buffer.asReadOnlyBuffer().get(bytes) OtlpJsonAnyValue.bytes(Base64.getEncoder().encodeToString(bytes)) } - ValueType.EMPTY -> OtlpJsonAnyValue.empty() } } diff --git a/sdk/@launchdarkly/observability-android/lib/src/main/kotlin/com/launchdarkly/observability/otlp/json/common/OtlpJsonCommonModels.kt b/sdk/@launchdarkly/observability-android/lib/src/main/kotlin/com/launchdarkly/observability/otlp/json/common/OtlpJsonCommonModels.kt index c6f30a3bb8..300f407c78 100644 --- a/sdk/@launchdarkly/observability-android/lib/src/main/kotlin/com/launchdarkly/observability/otlp/json/common/OtlpJsonCommonModels.kt +++ b/sdk/@launchdarkly/observability-android/lib/src/main/kotlin/com/launchdarkly/observability/otlp/json/common/OtlpJsonCommonModels.kt @@ -59,9 +59,6 @@ sealed class OtlpJsonAnyValue { data class BytesVal(val value: String) : OtlpJsonAnyValue() - /** An unset value, written as `{}` (proto `AnyValue` with no `oneof` member set). */ - object EmptyVal : OtlpJsonAnyValue() - data class ArrayWrapper(val values: List = emptyList()) data class KvListWrapper(val values: List = emptyList()) @@ -74,6 +71,5 @@ sealed class OtlpJsonAnyValue { fun array(values: List): OtlpJsonAnyValue = ArrayVal(ArrayWrapper(values)) fun kvlist(values: List): OtlpJsonAnyValue = KvListVal(KvListWrapper(values)) fun bytes(base64: String): OtlpJsonAnyValue = BytesVal(base64) - fun empty(): OtlpJsonAnyValue = EmptyVal } } diff --git a/sdk/@launchdarkly/observability-android/lib/src/main/kotlin/com/launchdarkly/observability/replay/InteractionMoveGrouper.kt b/sdk/@launchdarkly/observability-android/lib/src/main/kotlin/com/launchdarkly/observability/replay/InteractionMoveGrouper.kt index 746c96d088..7a8851d998 100644 --- a/sdk/@launchdarkly/observability-android/lib/src/main/kotlin/com/launchdarkly/observability/replay/InteractionMoveGrouper.kt +++ b/sdk/@launchdarkly/observability-android/lib/src/main/kotlin/com/launchdarkly/observability/replay/InteractionMoveGrouper.kt @@ -1,7 +1,7 @@ package com.launchdarkly.observability.replay import android.view.MotionEvent -import io.opentelemetry.android.session.SessionProvider +import io.opentelemetry.android.session.SessionManager import kotlinx.coroutines.flow.MutableSharedFlow private const val FILTER_THRESHOLD_DISTANCE_SQUARED_PIXELS = 144 // 12 X 12 pixels, matches iOS tapMaxDistanceSquared @@ -18,7 +18,7 @@ private const val EMIT_PERIOD_MILLIS = 240 // time to gather 4 positions into gr * avoid delays handing off data to collector of flow */ class InteractionMoveGrouper( - private val _sessionManager: SessionProvider, + private val _sessionManager: SessionManager, private val _bufferedFlow: MutableSharedFlow, ) { private val acceptedPositions = mutableListOf() diff --git a/sdk/@launchdarkly/observability-android/lib/src/main/kotlin/com/launchdarkly/observability/replay/InteractionSource.kt b/sdk/@launchdarkly/observability-android/lib/src/main/kotlin/com/launchdarkly/observability/replay/InteractionSource.kt index 5eb53e2160..dcf1ad0e69 100644 --- a/sdk/@launchdarkly/observability-android/lib/src/main/kotlin/com/launchdarkly/observability/replay/InteractionSource.kt +++ b/sdk/@launchdarkly/observability-android/lib/src/main/kotlin/com/launchdarkly/observability/replay/InteractionSource.kt @@ -2,7 +2,7 @@ package com.launchdarkly.observability.replay import android.view.MotionEvent import com.launchdarkly.observability.client.TouchSample -import io.opentelemetry.android.session.SessionProvider +import io.opentelemetry.android.session.SessionManager import kotlinx.coroutines.channels.BufferOverflow import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.SharedFlow @@ -23,7 +23,7 @@ import kotlinx.coroutines.flow.asSharedFlow * @param density the display density used to derive the pixel scale factor. */ class InteractionSource( - private val sessionManager: SessionProvider, + private val sessionManager: SessionManager, private val scale: Double?, private val density: Double, ) { diff --git a/sdk/@launchdarkly/observability-android/lib/src/main/kotlin/com/launchdarkly/observability/replay/SessionReplayService.kt b/sdk/@launchdarkly/observability-android/lib/src/main/kotlin/com/launchdarkly/observability/replay/SessionReplayService.kt index 7658a8eae7..a9239e4323 100644 --- a/sdk/@launchdarkly/observability-android/lib/src/main/kotlin/com/launchdarkly/observability/replay/SessionReplayService.kt +++ b/sdk/@launchdarkly/observability-android/lib/src/main/kotlin/com/launchdarkly/observability/replay/SessionReplayService.kt @@ -28,7 +28,7 @@ import com.launchdarkly.observability.replay.transport.EventQueue import com.launchdarkly.observability.context.LDObserveContext import com.launchdarkly.observability.sdk.SessionReplayServicing import com.launchdarkly.observability.util.requireMainThread -import io.opentelemetry.android.session.SessionProvider +import io.opentelemetry.android.session.SessionManager import io.opentelemetry.api.common.Attributes import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Job @@ -77,7 +77,7 @@ class SessionReplayService( private val imageCaptureService: ImageCaptureServicing? = null, ) : SessionReplayServicing { - private lateinit var sessionManager: SessionProvider + private lateinit var sessionManager: SessionManager private val logger: ObserveLogger = observabilityContext.logger private val eventQueue = EventQueue() private val batchWorker = BatchWorker(eventQueue, logger) diff --git a/sdk/@launchdarkly/observability-android/lib/src/main/kotlin/com/launchdarkly/observability/replay/capture/CaptureManager.kt b/sdk/@launchdarkly/observability-android/lib/src/main/kotlin/com/launchdarkly/observability/replay/capture/CaptureManager.kt index 2e20f7a6c2..9b7b423bab 100644 --- a/sdk/@launchdarkly/observability-android/lib/src/main/kotlin/com/launchdarkly/observability/replay/capture/CaptureManager.kt +++ b/sdk/@launchdarkly/observability-android/lib/src/main/kotlin/com/launchdarkly/observability/replay/capture/CaptureManager.kt @@ -2,7 +2,7 @@ package com.launchdarkly.observability.replay.capture import com.launchdarkly.observability.context.ObserveLogger import com.launchdarkly.observability.replay.ReplayOptions -import io.opentelemetry.android.session.SessionProvider +import io.opentelemetry.android.session.SessionManager import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.SharedFlow import kotlinx.coroutines.flow.asSharedFlow @@ -14,7 +14,7 @@ import kotlinx.coroutines.flow.asSharedFlow * @param sessionManager Used to get current session for tagging [ExportFrame] with session id */ class CaptureManager( - private val sessionManager: SessionProvider, + private val sessionManager: SessionManager, private val options: ReplayOptions, private val logger: ObserveLogger, private val imageCaptureService: ImageCaptureServicing = ImageCaptureService(options, logger), diff --git a/sdk/@launchdarkly/observability-android/lib/src/main/kotlin/io/opentelemetry/android/LDRumSessionManagerAccessor.kt b/sdk/@launchdarkly/observability-android/lib/src/main/kotlin/io/opentelemetry/android/LDRumSessionManagerAccessor.kt new file mode 100644 index 0000000000..3ba2a22081 --- /dev/null +++ b/sdk/@launchdarkly/observability-android/lib/src/main/kotlin/io/opentelemetry/android/LDRumSessionManagerAccessor.kt @@ -0,0 +1,17 @@ +package io.opentelemetry.android + +import io.opentelemetry.android.session.SessionManager + +/** + * Bridges to [OpenTelemetryRumBuilder.setSessionManager], which is package-private in + * OpenTelemetry Android. Declaring this object in the same `io.opentelemetry.android` package lets + * the LaunchDarkly SDK supply its own [SessionManager] (see + * [com.launchdarkly.observability.client.LDSessionManager]) so it backs the RUM SDK's `session.id` + * span/log appenders, rather than relying on the default manager captured via an instrumentation. + */ +internal object LDRumSessionManagerAccessor { + fun setSessionManager( + builder: OpenTelemetryRumBuilder, + sessionManager: SessionManager, + ): OpenTelemetryRumBuilder = builder.setSessionManager(sessionManager) +} diff --git a/sdk/@launchdarkly/observability-android/lib/src/test/kotlin/com/launchdarkly/observability/replay/InteractionMoveGrouperTest.kt b/sdk/@launchdarkly/observability-android/lib/src/test/kotlin/com/launchdarkly/observability/replay/InteractionMoveGrouperTest.kt index 1397a78f55..b8dc15f725 100644 --- a/sdk/@launchdarkly/observability-android/lib/src/test/kotlin/com/launchdarkly/observability/replay/InteractionMoveGrouperTest.kt +++ b/sdk/@launchdarkly/observability-android/lib/src/test/kotlin/com/launchdarkly/observability/replay/InteractionMoveGrouperTest.kt @@ -3,7 +3,7 @@ package com.launchdarkly.observability.replay import android.view.MotionEvent import io.mockk.every import io.mockk.mockk -import io.opentelemetry.android.session.SessionProvider +import io.opentelemetry.android.session.SessionManager import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.take import kotlinx.coroutines.flow.toList @@ -14,13 +14,13 @@ import org.junit.jupiter.api.Test class InteractionMoveGrouperTest { - private lateinit var mockSessionManager: SessionProvider + private lateinit var mockSessionManager: SessionManager private lateinit var bufferedFlow: MutableSharedFlow private lateinit var grouper: InteractionMoveGrouper @BeforeEach fun setUp() { - mockSessionManager = mockk(relaxed = true) + mockSessionManager = mockk(relaxed = true) every { mockSessionManager.getSessionId() } returns "test-session-id" bufferedFlow = MutableSharedFlow(replay = 64, extraBufferCapacity = 64) grouper = InteractionMoveGrouper(mockSessionManager, bufferedFlow) diff --git a/sdk/@launchdarkly/observability-android/lib/src/test/kotlin/com/launchdarkly/observability/replay/SessionReplayTest.kt b/sdk/@launchdarkly/observability-android/lib/src/test/kotlin/com/launchdarkly/observability/replay/SessionReplayTest.kt index cb6a061f4d..874dabcd20 100644 --- a/sdk/@launchdarkly/observability-android/lib/src/test/kotlin/com/launchdarkly/observability/replay/SessionReplayTest.kt +++ b/sdk/@launchdarkly/observability-android/lib/src/test/kotlin/com/launchdarkly/observability/replay/SessionReplayTest.kt @@ -57,7 +57,7 @@ class SessionReplayTest { @Test fun `initialize wires up LDReplay when service install succeeds`() { // Substitute a stub service for the one register() created so we can decide the - // SessionReplayService.initialize() outcome without standing up a real SessionProvider, + // SessionReplayService.initialize() outcome without standing up a real SessionManager, // ProcessLifecycleOwner, etc. — none of which are available in plain JVM tests. val service = mockk(relaxed = true) every { service.initialize() } returns true diff --git a/sdk/@launchdarkly/observability-android/lib/src/test/kotlin/com/launchdarkly/observability/replay/capture/CaptureManagerTest.kt b/sdk/@launchdarkly/observability-android/lib/src/test/kotlin/com/launchdarkly/observability/replay/capture/CaptureManagerTest.kt index ba4ad793f6..c7ab7323af 100644 --- a/sdk/@launchdarkly/observability-android/lib/src/test/kotlin/com/launchdarkly/observability/replay/capture/CaptureManagerTest.kt +++ b/sdk/@launchdarkly/observability-android/lib/src/test/kotlin/com/launchdarkly/observability/replay/capture/CaptureManagerTest.kt @@ -3,7 +3,7 @@ package com.launchdarkly.observability.replay.capture import com.launchdarkly.observability.context.ObserveLogger import com.launchdarkly.observability.replay.ReplayOptions import io.mockk.mockk -import io.opentelemetry.android.session.SessionProvider +import io.opentelemetry.android.session.SessionManager import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Test @@ -24,7 +24,7 @@ class CaptureManagerTest { } private fun captureManager(options: ReplayOptions = ReplayOptions()) = CaptureManager( - sessionManager = mockk(relaxed = true), + sessionManager = mockk(relaxed = true), options = options, logger = mockk(relaxed = true), imageCaptureService = mockk(relaxed = true), From 5cfe7ce271e174fb7969eb7eda53f610de604d0e Mon Sep 17 00:00:00 2001 From: Andrey Belonogov Date: Tue, 6 Oct 2026 08:59:54 -0700 Subject: [PATCH 6/9] feat(android): upgrade to OTel Java 1.66 and OTel Android 1.7 Restores the upgrade that was split out of #791 so the MAUI Release fix could ship without it. Not ready to merge as-is. OTel Android 1.7 pulls androidx.core 1.19, androidx.lifecycle 2.11 and okhttp-android 5.4, which declare a minimum compileSdk of 36-37 and AGP 9.1. The e2e app (compileSdk 35, AGP 8.13.2) fails checkComposeDebugAarMetadata on them, and so will any consumer still on AGP 8. The resolutionStrategy pins in lib/build.gradle.kts only hold inside this build; they are not published, so consumers see the raw transitives. Landing this needs an AGP 9 / compileSdk 37 migration first, or a decision to pin the transitives in the published POM. Co-authored-by: Cursor --- .../android/native/LDObserve/build.gradle.kts | 81 +++++++++++++------ .../observability/LDObservability.Fat.csproj | 9 ++- .../observability/NativeAndroidDeps.props | 25 +++++- .../lib/build.gradle.kts | 47 ++++++++--- .../observability/bridge/LDValueAttributes.kt | 1 + .../observability/client/LDSessionManager.kt | 57 +++++++++---- .../client/LaunchTimeInstrumentation.kt | 4 +- .../client/ObservabilityContext.kt | 4 +- .../client/ObservabilityService.kt | 24 +++--- .../otlp/json/OtlpJsonEncoding.kt | 1 + .../otlp/json/common/JsonCommonAdapter.kt | 3 + .../otlp/json/common/OtlpJsonCommonModels.kt | 4 + .../replay/InteractionMoveGrouper.kt | 4 +- .../observability/replay/InteractionSource.kt | 4 +- .../replay/SessionReplayService.kt | 4 +- .../replay/capture/CaptureManager.kt | 4 +- .../android/LDRumSessionManagerAccessor.kt | 17 ---- .../replay/InteractionMoveGrouperTest.kt | 6 +- .../observability/replay/SessionReplayTest.kt | 2 +- .../replay/capture/CaptureManagerTest.kt | 4 +- 20 files changed, 199 insertions(+), 106 deletions(-) delete mode 100644 sdk/@launchdarkly/observability-android/lib/src/main/kotlin/io/opentelemetry/android/LDRumSessionManagerAccessor.kt diff --git a/sdk/@launchdarkly/mobile-dotnet/android/native/LDObserve/build.gradle.kts b/sdk/@launchdarkly/mobile-dotnet/android/native/LDObserve/build.gradle.kts index 6a28584707..9c97665f2a 100644 --- a/sdk/@launchdarkly/mobile-dotnet/android/native/LDObserve/build.gradle.kts +++ b/sdk/@launchdarkly/mobile-dotnet/android/native/LDObserve/build.gradle.kts @@ -55,6 +55,32 @@ configurations { create("copyDependencies") } +// Mirrors the pins in observability-android: OTel Android 1.7 drags in kotlinx-coroutines 1.11.0 +// (Kotlin 2.2 metadata, unreadable by the 2.0.21 compiler) and androidx.core 1.19.0 (needs AGP 9.1 +// / compileSdk 37). Both are held back to the newest releases this toolchain can build against. +configurations.all { + resolutionStrategy.eachDependency { + val name = requested.name + // OTel Android 1.7 depends on kotlin-stdlib 2.4.10, whose metadata this module's 2.0.21 + // compiler cannot read. Hold the Kotlin runtime at the compiler's own version. + val isKotlinRuntime = name.startsWith("kotlin-stdlib") || + name == "kotlin-reflect" || + name.startsWith("kotlin-test") + if (requested.group == "org.jetbrains.kotlin" && isKotlinRuntime) { + useVersion("2.0.21") + } + if (requested.group == "org.jetbrains.kotlinx" && name.startsWith("kotlinx-coroutines")) { + useVersion("1.10.2") + } + if (requested.group == "androidx.core" && (name == "core" || name == "core-ktx")) { + useVersion("1.16.0") + } + if (requested.group == "com.squareup.okhttp3") { + useVersion("5.2.1") + } + } +} + dependencies { implementation("androidx.core:core-ktx:1.15.0") "copyDependencies"("androidx.core:core-ktx:1.15.0") @@ -67,45 +93,52 @@ dependencies { // TODO: revise these versions to be as old as usable for compatibility // OpenTelemetry JARs copied here are filtered for NuGet in observability/LDObservability.Fat.csproj (autoconfigure vs autoconfigure-spi). - implementation("io.opentelemetry:opentelemetry-api:1.51.0") - "copyDependencies"("io.opentelemetry:opentelemetry-api:1.51.0") + implementation("io.opentelemetry:opentelemetry-api:1.66.0") + "copyDependencies"("io.opentelemetry:opentelemetry-api:1.66.0") - implementation("io.opentelemetry:opentelemetry-sdk:1.51.0") - "copyDependencies"("io.opentelemetry:opentelemetry-sdk:1.51.0") + implementation("io.opentelemetry:opentelemetry-sdk:1.66.0") + "copyDependencies"("io.opentelemetry:opentelemetry-sdk:1.66.0") - implementation("io.opentelemetry:opentelemetry-exporter-otlp:1.51.0") - "copyDependencies"("io.opentelemetry:opentelemetry-exporter-otlp:1.51.0") + implementation("io.opentelemetry:opentelemetry-exporter-otlp:1.66.0") + "copyDependencies"("io.opentelemetry:opentelemetry-exporter-otlp:1.66.0") - implementation("io.opentelemetry:opentelemetry-exporter-sender-okhttp:1.51.0") - "copyDependencies"("io.opentelemetry:opentelemetry-exporter-sender-okhttp:1.51.0") + implementation("io.opentelemetry:opentelemetry-exporter-sender-okhttp:1.66.0") + "copyDependencies"("io.opentelemetry:opentelemetry-exporter-sender-okhttp:1.66.0") - implementation("io.opentelemetry:opentelemetry-exporter-logging-otlp:1.51.0") - "copyDependencies"("io.opentelemetry:opentelemetry-exporter-logging-otlp:1.51.0") + implementation("io.opentelemetry:opentelemetry-exporter-logging-otlp:1.66.0") + "copyDependencies"("io.opentelemetry:opentelemetry-exporter-logging-otlp:1.66.0") - implementation("io.opentelemetry:opentelemetry-sdk-metrics:1.51.0") - "copyDependencies"("io.opentelemetry:opentelemetry-sdk-metrics:1.51.0") + implementation("io.opentelemetry:opentelemetry-sdk-metrics:1.66.0") + "copyDependencies"("io.opentelemetry:opentelemetry-sdk-metrics:1.66.0") - implementation("io.opentelemetry:opentelemetry-sdk-logs:1.51.0") - "copyDependencies"("io.opentelemetry:opentelemetry-sdk-logs:1.51.0") + implementation("io.opentelemetry:opentelemetry-sdk-logs:1.66.0") + "copyDependencies"("io.opentelemetry:opentelemetry-sdk-logs:1.66.0") // TODO: Evaluate risks associated with incubator APIs - implementation("io.opentelemetry:opentelemetry-api-incubator:1.51.0-alpha") - "copyDependencies"("io.opentelemetry:opentelemetry-api-incubator:1.51.0-alpha") + implementation("io.opentelemetry:opentelemetry-api-incubator:1.66.0-alpha") + "copyDependencies"("io.opentelemetry:opentelemetry-api-incubator:1.66.0-alpha") // OTEL Android - implementation("io.opentelemetry.android:core:0.11.0-alpha") - "copyDependencies"("io.opentelemetry.android:core:0.11.0-alpha") + implementation("io.opentelemetry.android:core:1.7.0-alpha") + "copyDependencies"("io.opentelemetry.android:core:1.7.0-alpha") + + implementation("io.opentelemetry.android:session:1.7.0") + "copyDependencies"("io.opentelemetry.android:session:1.7.0") + + // `OpenTelemetryRum` and `AndroidInstrumentation` moved out of :core in 1.x. + implementation("io.opentelemetry.android:agent-api:1.7.0") + "copyDependencies"("io.opentelemetry.android:agent-api:1.7.0") - implementation("io.opentelemetry.android:session:0.11.0-alpha") - "copyDependencies"("io.opentelemetry.android:session:0.11.0-alpha") + implementation("io.opentelemetry.android.instrumentation:android-instrumentation:1.7.0") + "copyDependencies"("io.opentelemetry.android.instrumentation:android-instrumentation:1.7.0") // OTEL Android Instrumentations - implementation("io.opentelemetry.android.instrumentation:crash:0.11.0-alpha") - "copyDependencies"("io.opentelemetry.android.instrumentation:crash:0.11.0-alpha") + implementation("io.opentelemetry.android.instrumentation:crash:1.7.0-alpha") + "copyDependencies"("io.opentelemetry.android.instrumentation:crash:1.7.0-alpha") - implementation("io.opentelemetry.android:android-agent:0.11.0-alpha") - "copyDependencies"("io.opentelemetry.android:android-agent:0.11.0-alpha") + implementation("io.opentelemetry.android:android-agent:1.7.0") + "copyDependencies"("io.opentelemetry.android:android-agent:1.7.0") } // Custom task: merges all transitive JARs (OTel + okhttp + okio + ...) into a diff --git a/sdk/@launchdarkly/mobile-dotnet/observability/LDObservability.Fat.csproj b/sdk/@launchdarkly/mobile-dotnet/observability/LDObservability.Fat.csproj index 8675fa53b1..359013ccd0 100644 --- a/sdk/@launchdarkly/mobile-dotnet/observability/LDObservability.Fat.csproj +++ b/sdk/@launchdarkly/mobile-dotnet/observability/LDObservability.Fat.csproj @@ -28,10 +28,11 @@ - diff --git a/sdk/@launchdarkly/mobile-dotnet/observability/NativeAndroidDeps.props b/sdk/@launchdarkly/mobile-dotnet/observability/NativeAndroidDeps.props index 325b34f8a5..a77a25c5dd 100644 --- a/sdk/@launchdarkly/mobile-dotnet/observability/NativeAndroidDeps.props +++ b/sdk/@launchdarkly/mobile-dotnet/observability/NativeAndroidDeps.props @@ -18,20 +18,39 @@ - + + <_LDNativeAAR Include="$(_LDNativeDepsDir)agent-api-*.aar" /> <_LDNativeAAR Include="$(_LDNativeDepsDir)android-agent-*.aar" /> <_LDNativeAAR Include="$(_LDNativeDepsDir)android-instrumentation-*.aar" /> - <_LDNativeAAR Include="$(_LDNativeDepsDir)common-0*.aar" /> + + <_LDNativeAAR Include="$(_LDNativeDepsDir)common-1*.aar" /> <_LDNativeAAR Include="$(_LDNativeDepsDir)common-api-*.aar" /> <_LDNativeAAR Include="$(_LDNativeDepsDir)core-*-alpha.aar" /> <_LDNativeAAR Include="$(_LDNativeDepsDir)anr-*-alpha.aar" /> <_LDNativeAAR Include="$(_LDNativeDepsDir)activity-*-alpha.aar" /> <_LDNativeAAR Include="$(_LDNativeDepsDir)fragment-*-alpha.aar" /> <_LDNativeAAR Include="$(_LDNativeDepsDir)network-*-alpha.aar" /> + <_LDNativeAAR Include="$(_LDNativeDepsDir)screen-orientation-*-alpha.aar" /> <_LDNativeAAR Include="$(_LDNativeDepsDir)services-*-alpha.aar" /> - <_LDNativeAAR Include="$(_LDNativeDepsDir)session-*-alpha.aar" /> + + <_LDNativeAAR Include="$(_LDNativeDepsDir)session-*.aar" /> + <_LDNativeAAR Include="$(_LDNativeDepsDir)sessions-*-alpha.aar" /> <_LDNativeAAR Include="$(_LDNativeDepsDir)slowrendering-*-alpha.aar" /> <_LDNativeAAR Include="$(_LDNativeDepsDir)startup-*-alpha.aar" /> + + <_LDNativeAAR Include="$(_LDNativeDepsDir)semconv-*-alpha.aar" /> + + <_LDNativeAAR Include="$(_LDNativeDepsDir)semconv.aar" /> <_LDNativeAAR Include="$(_LDNativeDepsDir)lib-release.aar" /> diff --git a/sdk/@launchdarkly/observability-android/lib/build.gradle.kts b/sdk/@launchdarkly/observability-android/lib/build.gradle.kts index 7f803fc092..e033acb0a6 100644 --- a/sdk/@launchdarkly/observability-android/lib/build.gradle.kts +++ b/sdk/@launchdarkly/observability-android/lib/build.gradle.kts @@ -31,7 +31,7 @@ val isClientSdkProvidedByHost = // Pin Kotlin runtime artifacts on this build's classpath to match the configured Kotlin // compiler version (2.0.21). Without this, transitive deps such as the -// io.opentelemetry.android:*:0.11.0-alpha modules drag in newer kotlin-stdlib / kotlin-reflect +// io.opentelemetry.android:*:1.7.0 modules drag in newer kotlin-stdlib / kotlin-reflect // whose metadata version the 2.0.21 compiler cannot read, producing // "Module was compiled with an incompatible version of Kotlin" errors at compileDebugKotlin. // @@ -50,6 +50,26 @@ configurations.all { useVersion("2.0.21") because("Align Kotlin runtime artifacts with the project's Kotlin compiler version (2.0.21).") } + // io.opentelemetry.android:*:1.7.0 pulls androidx.lifecycle 2.11.0, which in turn requires + // kotlinx-coroutines 1.11.0. That release carries Kotlin metadata 2.2.0, and the 2.0.21 + // compiler reads at most 2.1.0. 1.10.2 is the newest release still on metadata 2.1.0. + if (requested.group == "org.jetbrains.kotlinx" && name.startsWith("kotlinx-coroutines")) { + useVersion("1.10.2") + because("kotlinx-coroutines 1.11.0 carries Kotlin 2.2 metadata the 2.0.21 compiler cannot read.") + } + // io.opentelemetry.android:common:1.7.0-alpha pulls androidx.core 1.19.0, which refuses to + // build on anything below AGP 9.1 / compileSdk 37. Hold it at the newest release that still + // builds against compileSdk 35 with AGP 8.x. + if (requested.group == "androidx.core" && (name == "core" || name == "core-ktx")) { + useVersion("1.16.0") + because("androidx.core 1.17.0+ requires a compileSdk/AGP combination this project does not target yet.") + } + // Same story for okhttp: the OTel 1.66 okhttp sender asks for 5.5.0, whose Android variant + // requires compileSdk 37. + if (requested.group == "com.squareup.okhttp3") { + useVersion("5.2.1") + because("okhttp-android 5.3.0+ requires compileSdk 37, beyond what AGP 8.x supports here.") + } } } @@ -73,28 +93,33 @@ dependencies { implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.9.0") // TODO: revise these versions to be as old as usable for compatibility - implementation("io.opentelemetry:opentelemetry-api:1.51.0") - implementation("io.opentelemetry:opentelemetry-sdk:1.51.0") - implementation("io.opentelemetry:opentelemetry-sdk-metrics:1.51.0") - implementation("io.opentelemetry:opentelemetry-sdk-logs:1.51.0") + implementation("io.opentelemetry:opentelemetry-api:1.66.0") + implementation("io.opentelemetry:opentelemetry-sdk:1.66.0") + implementation("io.opentelemetry:opentelemetry-sdk-metrics:1.66.0") + implementation("io.opentelemetry:opentelemetry-sdk-logs:1.66.0") // Required at runtime by io.opentelemetry.android:core, which uses incubator APIs // internally for the logs bridge. Can be removed once the OTel Android SDK drops this dependency. - implementation("io.opentelemetry:opentelemetry-api-incubator:1.51.0-alpha") + implementation("io.opentelemetry:opentelemetry-api-incubator:1.66.0-alpha") // OTEL Android - implementation("io.opentelemetry.android:core:0.11.0-alpha") - implementation("io.opentelemetry.android:session:0.11.0-alpha") + implementation("io.opentelemetry.android:core:1.7.0-alpha") + implementation("io.opentelemetry.android:session:1.7.0") + + // `OpenTelemetryRum` and `AndroidInstrumentation` moved out of :core in 1.x and are only + // runtime-scoped transitives of it, so they must be declared to be on the compile classpath. + implementation("io.opentelemetry.android:agent-api:1.7.0") + implementation("io.opentelemetry.android.instrumentation:android-instrumentation:1.7.0") // OTEL Android Instrumentations - implementation("io.opentelemetry.android.instrumentation:crash:0.11.0-alpha") + implementation("io.opentelemetry.android.instrumentation:crash:1.7.0-alpha") // NOTE: the `activity` instrumentation is intentionally NOT depended on. It is superseded by // LaunchDarkly's own app/screen lifecycle spans and would otherwise double-report; we also // defensively suppress it by name (see ObservabilityService.createOtelRumConfig). // Use JUnit Jupiter for testing. // Testing exporters for telemetry inspection - testImplementation("io.opentelemetry:opentelemetry-sdk-testing:1.51.0") + testImplementation("io.opentelemetry:opentelemetry-sdk-testing:1.66.0") testImplementation(platform("org.junit:junit-bom:5.13.4")) testImplementation("org.junit.jupiter:junit-jupiter") testRuntimeOnly("org.junit.platform:junit-platform-launcher") @@ -104,7 +129,7 @@ dependencies { testImplementation("io.mockk:mockk:1.14.5") testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.9.0") - testFixturesApi("io.opentelemetry:opentelemetry-sdk-testing:1.51.0") + testFixturesApi("io.opentelemetry:opentelemetry-sdk-testing:1.66.0") testFixturesImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.9.0") } diff --git a/sdk/@launchdarkly/observability-android/lib/src/main/kotlin/com/launchdarkly/observability/bridge/LDValueAttributes.kt b/sdk/@launchdarkly/observability-android/lib/src/main/kotlin/com/launchdarkly/observability/bridge/LDValueAttributes.kt index a2c3daedd7..d5d104038b 100644 --- a/sdk/@launchdarkly/observability-android/lib/src/main/kotlin/com/launchdarkly/observability/bridge/LDValueAttributes.kt +++ b/sdk/@launchdarkly/observability-android/lib/src/main/kotlin/com/launchdarkly/observability/bridge/LDValueAttributes.kt @@ -112,4 +112,5 @@ private fun reKey(original: AttributeKey<*>, newName: String): AttributeKey<*> = AttributeType.BOOLEAN_ARRAY -> AttributeKey.booleanArrayKey(newName) AttributeType.LONG_ARRAY -> AttributeKey.longArrayKey(newName) AttributeType.DOUBLE_ARRAY -> AttributeKey.doubleArrayKey(newName) + AttributeType.VALUE -> AttributeKey.valueKey(newName) } diff --git a/sdk/@launchdarkly/observability-android/lib/src/main/kotlin/com/launchdarkly/observability/client/LDSessionManager.kt b/sdk/@launchdarkly/observability-android/lib/src/main/kotlin/com/launchdarkly/observability/client/LDSessionManager.kt index 38a378d862..7c9df01a64 100644 --- a/sdk/@launchdarkly/observability-android/lib/src/main/kotlin/com/launchdarkly/observability/client/LDSessionManager.kt +++ b/sdk/@launchdarkly/observability-android/lib/src/main/kotlin/com/launchdarkly/observability/client/LDSessionManager.kt @@ -1,28 +1,29 @@ package com.launchdarkly.observability.client import io.opentelemetry.android.session.Session -import io.opentelemetry.android.session.SessionIdGenerator -import io.opentelemetry.android.session.SessionManager import io.opentelemetry.android.session.SessionObserver +import io.opentelemetry.android.session.SessionProvider +import io.opentelemetry.android.session.SessionPublisher import io.opentelemetry.sdk.common.Clock +import java.security.SecureRandom import java.util.Collections.synchronizedList import kotlin.time.Duration /** - * LaunchDarkly's own [SessionManager], used in place of OpenTelemetry Android's default so we can + * LaunchDarkly's own session source, used in place of OpenTelemetry Android's default so we can * **seed the initial session id** ([initialSessionId]). This lets the native observability instance * adopt a session id created elsewhere (e.g. by the JavaScript SDK on the same device), so that * spans, logs, metrics, and Session Replay all report the same `session.id`. * - * Injected into [io.opentelemetry.android.OpenTelemetryRumBuilder] via - * [io.opentelemetry.android.LDRumSessionManagerAccessor] so it backs OpenTelemetry Android's own - * `session.id` span/log appenders as well — making it the single source of session identity. + * Injected into [io.opentelemetry.android.OpenTelemetryRumBuilder] via `setSessionProvider` so it + * backs OpenTelemetry Android's own `session.id` span/log appenders as well — making it the single + * source of session identity. * * When [initialSessionId] is supplied the session is treated as *custom*: the caller owns the * session lifecycle, so automatic rotation is disabled and the seeded id is used for the lifetime * of this manager (mirrors iOS's `isCustomSession`). * - * Otherwise rotation mirrors OpenTelemetry Android's default `SessionManagerImpl` / + * Otherwise rotation mirrors OpenTelemetry Android's default session manager / * `SessionIdTimeoutHandler`: * - a foreground session never times out; * - a background gap of at least [backgroundInactivityTimeout] rotates the session on next use; @@ -37,15 +38,15 @@ import kotlin.time.Duration * @param backgroundInactivityTimeout Background inactivity after which the session rotates. * @param maxLifetime Absolute maximum session lifetime before rotation. * @param clock Time source; defaults to the OpenTelemetry default clock. - * @param idGenerator Generator for new session ids; defaults to OpenTelemetry's. + * @param idGenerator Generator for new session ids; defaults to a random 128-bit hex id. */ internal class LDSessionManager( initialSessionId: String? = null, private val backgroundInactivityTimeout: Duration, private val maxLifetime: Duration, private val clock: Clock = Clock.getDefault(), - private val idGenerator: SessionIdGenerator = SessionIdGenerator.DEFAULT, -) : SessionManager { + private val idGenerator: () -> String = ::randomSessionId, +) : SessionProvider, SessionPublisher { private val lock = Any() private val observers = synchronizedList(ArrayList()) @@ -60,11 +61,11 @@ internal class LDSessionManager( // Guarded by [lock]. private var session: Session = if (isCustomSession) { - Session.DefaultSession(initialSessionId!!, clock.now()) + LDSession(initialSessionId!!, clock.now()) } else { - // Empty id + (-1) timestamp forces generation on first getSessionId(), exactly like - // the default manager's initial Session.NONE. - Session.NONE + // Empty id + zero timestamp forces generation on first read, exactly like the default + // manager's initial "none" session. + NONE_SESSION } // Timeout bookkeeping, mirroring SessionIdTimeoutHandler. @@ -87,7 +88,7 @@ internal class LDSessionManager( var candidate = session // An externally supplied session id is never rotated; the caller owns its lifecycle. if (!isCustomSession && (sessionHasExpired() || hasTimedOut())) { - candidate = Session.DefaultSession(idGenerator.generateSessionId(), clock.now()) + candidate = LDSession(idGenerator(), clock.now()) } // Bump the inactivity timer after deciding, before notifying (a new span may be created). bump() @@ -106,7 +107,7 @@ internal class LDSessionManager( observer.onSessionStarted(newSession, previousSession) } } - return newSession.getId() + return newSession.id } /** Marks the app as transitioning to the foreground; the next event settles it to foreground. */ @@ -120,7 +121,7 @@ internal class LDSessionManager( } private fun sessionHasExpired(): Boolean { - val elapsed = clock.now() - session.getStartTimestamp() + val elapsed = clock.now() - session.startTimestamp return elapsed >= maxLifetime.inWholeNanoseconds } @@ -146,4 +147,26 @@ internal class LDSessionManager( /** Temporary state for the first event after the app is brought back to the foreground. */ TRANSITIONING_TO_FOREGROUND, } + + private class LDSession( + override val id: String, + override val startTimestamp: Long, + ) : Session + + private companion object { + /** + * Stands in for the default manager's "no session yet" value. The zero start timestamp + * makes the first expiry check rotate immediately, generating the real id lazily. + */ + private val NONE_SESSION: Session = LDSession("", 0L) + } +} + +private val sessionIdRandom = SecureRandom() + +/** Generates a random 128-bit session id, rendered as 32 lowercase hex characters. */ +private fun randomSessionId(): String { + val bytes = ByteArray(16) + sessionIdRandom.nextBytes(bytes) + return bytes.joinToString("") { "%02x".format(it) } } diff --git a/sdk/@launchdarkly/observability-android/lib/src/main/kotlin/com/launchdarkly/observability/client/LaunchTimeInstrumentation.kt b/sdk/@launchdarkly/observability-android/lib/src/main/kotlin/com/launchdarkly/observability/client/LaunchTimeInstrumentation.kt index 8d0be0a959..687fc0e0af 100644 --- a/sdk/@launchdarkly/observability-android/lib/src/main/kotlin/com/launchdarkly/observability/client/LaunchTimeInstrumentation.kt +++ b/sdk/@launchdarkly/observability-android/lib/src/main/kotlin/com/launchdarkly/observability/client/LaunchTimeInstrumentation.kt @@ -11,8 +11,8 @@ import android.view.View import android.view.ViewTreeObserver.OnDrawListener import androidx.activity.FullyDrawnReporterOwner import com.launchdarkly.observability.interfaces.Metric +import io.opentelemetry.android.OpenTelemetryRum import io.opentelemetry.android.instrumentation.AndroidInstrumentation -import io.opentelemetry.android.instrumentation.InstallationContext import io.opentelemetry.api.common.Attributes import java.lang.ref.WeakReference import java.util.WeakHashMap @@ -44,7 +44,7 @@ internal class LaunchTimeInstrumentation( override val name: String = INSTRUMENTATION_SCOPE_NAME - override fun install(ctx: InstallationContext) { + override fun install(context: android.content.Context, rum: OpenTelemetryRum) { application.registerActivityLifecycleCallbacks(this) } diff --git a/sdk/@launchdarkly/observability-android/lib/src/main/kotlin/com/launchdarkly/observability/client/ObservabilityContext.kt b/sdk/@launchdarkly/observability-android/lib/src/main/kotlin/com/launchdarkly/observability/client/ObservabilityContext.kt index 7d2f1a2c4d..ae787e4b89 100644 --- a/sdk/@launchdarkly/observability-android/lib/src/main/kotlin/com/launchdarkly/observability/client/ObservabilityContext.kt +++ b/sdk/@launchdarkly/observability-android/lib/src/main/kotlin/com/launchdarkly/observability/client/ObservabilityContext.kt @@ -5,7 +5,7 @@ import com.launchdarkly.observability.api.ObservabilityOptions import com.launchdarkly.observability.client.screen.ScreenViewEvent import com.launchdarkly.observability.client.screen.ScreenViewManager import com.launchdarkly.observability.context.ObserveLogger -import io.opentelemetry.android.session.SessionManager +import io.opentelemetry.android.session.SessionProvider import io.opentelemetry.api.common.Attributes import kotlinx.coroutines.flow.SharedFlow @@ -17,7 +17,7 @@ data class ObservabilityContext( val options: ObservabilityOptions, val application: Application, val logger: ObserveLogger, - var sessionManager: SessionManager? = null, + var sessionManager: SessionProvider? = null, var resourceAttributes: Attributes = Attributes.empty(), /** * The single touch-capture hook owned by Observability. Session Replay consumes its diff --git a/sdk/@launchdarkly/observability-android/lib/src/main/kotlin/com/launchdarkly/observability/client/ObservabilityService.kt b/sdk/@launchdarkly/observability-android/lib/src/main/kotlin/com/launchdarkly/observability/client/ObservabilityService.kt index fa099f6c45..78e64d3857 100644 --- a/sdk/@launchdarkly/observability-android/lib/src/main/kotlin/com/launchdarkly/observability/client/ObservabilityService.kt +++ b/sdk/@launchdarkly/observability-android/lib/src/main/kotlin/com/launchdarkly/observability/client/ObservabilityService.kt @@ -35,13 +35,13 @@ import com.launchdarkly.observability.sampling.SamplingLogProcessor import com.launchdarkly.observability.traces.EventSpanProcessor import com.launchdarkly.observability.traces.OtlpTraceExporter import com.launchdarkly.observability.util.requireMainThread -import io.opentelemetry.android.LDRumSessionManagerAccessor import io.opentelemetry.android.OpenTelemetryRum import io.opentelemetry.android.OpenTelemetryRumBuilder +import io.opentelemetry.android.RumBuilder import io.opentelemetry.android.config.OtelRumConfig import io.opentelemetry.android.session.Session -import io.opentelemetry.android.session.SessionManager import io.opentelemetry.android.session.SessionObserver +import io.opentelemetry.android.session.SessionProvider import io.opentelemetry.api.common.AttributeKey import io.opentelemetry.api.common.Attributes import io.opentelemetry.api.logs.Logger @@ -106,7 +106,7 @@ class ObservabilityService( private val otelRUM: OpenTelemetryRum // LaunchDarkly's own session manager. Owns session identity for all signals (it also backs the - // RUM SDK's `session.id` appenders via LDRumSessionManagerAccessor) and can be seeded with + // RUM SDK's `session.id` appenders via `setSessionProvider`) and can be seeded with // [customSessionId]. Exposed through [sessionManager] for Session Replay. private val ldSessionManager = LDSessionManager( initialSessionId = customSessionId, @@ -114,7 +114,7 @@ class ObservabilityService( maxLifetime = 4.hours, ) - var sessionManager: SessionManager? = ldSessionManager + var sessionManager: SessionProvider? = ldSessionManager private set private var otelMeter: Meter private var otelLogger: Logger @@ -243,7 +243,7 @@ class ObservabilityService( } val otelRumConfig = createOtelRumConfig() - val rumBuilder = OpenTelemetryRum.builder(application, otelRumConfig) + val rumBuilder = RumBuilder.builder(application, otelRumConfig) .addLoggerProviderCustomizer { sdkLoggerProviderBuilder, _ -> return@addLoggerProviderCustomizer configureLoggerProvider(sdkLoggerProviderBuilder) } @@ -254,9 +254,9 @@ class ObservabilityService( return@addMeterProviderCustomizer configureMeterProvider(sdkMeterProviderBuilder) } - // Use our own session manager (instead of the RUM SDK's default) so we can seed the session + // Use our own session provider (instead of the RUM SDK's default) so we can seed the session // id and keep a single source of session identity across spans, logs, metrics, and replay. - LDRumSessionManagerAccessor.setSessionManager(rumBuilder, ldSessionManager) + rumBuilder.setSessionProvider(ldSessionManager) if (observabilityOptions.enabled && observabilityOptions.instrumentations.launchTime) { addLaunchTimeInstrumentation(rumBuilder) @@ -269,12 +269,12 @@ class ObservabilityService( // previous_screen against the prior session, and a re-appearing first screen would be // deduped instead of emitting a fresh navigation. // - // Only reset on an actual session *change*. The initial session start carries - // Session.NONE (empty id) as the previous session; resetting on it would clobber a first + // Only reset on an actual session *change*. The initial session start carries the "none" + // session (empty id) as the previous session; resetting on it would clobber a first // screen that may already have been recorded by the time this notification fires. - sessionManager?.addObserver(object : SessionObserver { + ldSessionManager.addObserver(object : SessionObserver { override fun onSessionStarted(newSession: Session, previousSession: Session) { - if (previousSession.getId().isNotEmpty()) { + if (previousSession.id.isNotEmpty()) { screenStack.reset() // Re-seed the new session with the screen the user is still viewing. No // onActivityResumed fires for an already-resumed activity, so without this the @@ -458,7 +458,7 @@ class ObservabilityService( private fun createOtelRumConfig(): OtelRumConfig { // Session lifetime/rotation is owned by [ldSessionManager] (injected via - // [LDRumSessionManagerAccessor]), so no SessionConfig is applied here. + // `OpenTelemetryRumBuilder.setSessionProvider`), so no SessionConfig is applied here. val config = OtelRumConfig() if (!observabilityOptions.enabled || !observabilityOptions.instrumentations.crashReporting) { diff --git a/sdk/@launchdarkly/observability-android/lib/src/main/kotlin/com/launchdarkly/observability/otlp/json/OtlpJsonEncoding.kt b/sdk/@launchdarkly/observability-android/lib/src/main/kotlin/com/launchdarkly/observability/otlp/json/OtlpJsonEncoding.kt index 0ad96510a8..d7ece34fb4 100644 --- a/sdk/@launchdarkly/observability-android/lib/src/main/kotlin/com/launchdarkly/observability/otlp/json/OtlpJsonEncoding.kt +++ b/sdk/@launchdarkly/observability-android/lib/src/main/kotlin/com/launchdarkly/observability/otlp/json/OtlpJsonEncoding.kt @@ -112,6 +112,7 @@ private fun JsonByteWriter.write(value: OtlpJsonAnyValue) { endObject() } is OtlpJsonAnyValue.BytesVal -> name("bytesValue").value(value.value) + is OtlpJsonAnyValue.EmptyVal -> Unit } endObject() } diff --git a/sdk/@launchdarkly/observability-android/lib/src/main/kotlin/com/launchdarkly/observability/otlp/json/common/JsonCommonAdapter.kt b/sdk/@launchdarkly/observability-android/lib/src/main/kotlin/com/launchdarkly/observability/otlp/json/common/JsonCommonAdapter.kt index 4e4db6e31c..bf790574fc 100644 --- a/sdk/@launchdarkly/observability-android/lib/src/main/kotlin/com/launchdarkly/observability/otlp/json/common/JsonCommonAdapter.kt +++ b/sdk/@launchdarkly/observability-android/lib/src/main/kotlin/com/launchdarkly/observability/otlp/json/common/JsonCommonAdapter.kt @@ -68,6 +68,8 @@ object JsonCommonAdapter { @Suppress("UNCHECKED_CAST") (value as List).map { OtlpJsonAnyValue.double(it) } ) + // Extended attributes carry a nested Value, which the Value overload already handles. + AttributeType.VALUE -> toJsonAnyValue(value as Value<*>) } /** @@ -99,5 +101,6 @@ object JsonCommonAdapter { buffer.asReadOnlyBuffer().get(bytes) OtlpJsonAnyValue.bytes(Base64.getEncoder().encodeToString(bytes)) } + ValueType.EMPTY -> OtlpJsonAnyValue.empty() } } diff --git a/sdk/@launchdarkly/observability-android/lib/src/main/kotlin/com/launchdarkly/observability/otlp/json/common/OtlpJsonCommonModels.kt b/sdk/@launchdarkly/observability-android/lib/src/main/kotlin/com/launchdarkly/observability/otlp/json/common/OtlpJsonCommonModels.kt index 300f407c78..c6f30a3bb8 100644 --- a/sdk/@launchdarkly/observability-android/lib/src/main/kotlin/com/launchdarkly/observability/otlp/json/common/OtlpJsonCommonModels.kt +++ b/sdk/@launchdarkly/observability-android/lib/src/main/kotlin/com/launchdarkly/observability/otlp/json/common/OtlpJsonCommonModels.kt @@ -59,6 +59,9 @@ sealed class OtlpJsonAnyValue { data class BytesVal(val value: String) : OtlpJsonAnyValue() + /** An unset value, written as `{}` (proto `AnyValue` with no `oneof` member set). */ + object EmptyVal : OtlpJsonAnyValue() + data class ArrayWrapper(val values: List = emptyList()) data class KvListWrapper(val values: List = emptyList()) @@ -71,5 +74,6 @@ sealed class OtlpJsonAnyValue { fun array(values: List): OtlpJsonAnyValue = ArrayVal(ArrayWrapper(values)) fun kvlist(values: List): OtlpJsonAnyValue = KvListVal(KvListWrapper(values)) fun bytes(base64: String): OtlpJsonAnyValue = BytesVal(base64) + fun empty(): OtlpJsonAnyValue = EmptyVal } } diff --git a/sdk/@launchdarkly/observability-android/lib/src/main/kotlin/com/launchdarkly/observability/replay/InteractionMoveGrouper.kt b/sdk/@launchdarkly/observability-android/lib/src/main/kotlin/com/launchdarkly/observability/replay/InteractionMoveGrouper.kt index 7a8851d998..746c96d088 100644 --- a/sdk/@launchdarkly/observability-android/lib/src/main/kotlin/com/launchdarkly/observability/replay/InteractionMoveGrouper.kt +++ b/sdk/@launchdarkly/observability-android/lib/src/main/kotlin/com/launchdarkly/observability/replay/InteractionMoveGrouper.kt @@ -1,7 +1,7 @@ package com.launchdarkly.observability.replay import android.view.MotionEvent -import io.opentelemetry.android.session.SessionManager +import io.opentelemetry.android.session.SessionProvider import kotlinx.coroutines.flow.MutableSharedFlow private const val FILTER_THRESHOLD_DISTANCE_SQUARED_PIXELS = 144 // 12 X 12 pixels, matches iOS tapMaxDistanceSquared @@ -18,7 +18,7 @@ private const val EMIT_PERIOD_MILLIS = 240 // time to gather 4 positions into gr * avoid delays handing off data to collector of flow */ class InteractionMoveGrouper( - private val _sessionManager: SessionManager, + private val _sessionManager: SessionProvider, private val _bufferedFlow: MutableSharedFlow, ) { private val acceptedPositions = mutableListOf() diff --git a/sdk/@launchdarkly/observability-android/lib/src/main/kotlin/com/launchdarkly/observability/replay/InteractionSource.kt b/sdk/@launchdarkly/observability-android/lib/src/main/kotlin/com/launchdarkly/observability/replay/InteractionSource.kt index dcf1ad0e69..5eb53e2160 100644 --- a/sdk/@launchdarkly/observability-android/lib/src/main/kotlin/com/launchdarkly/observability/replay/InteractionSource.kt +++ b/sdk/@launchdarkly/observability-android/lib/src/main/kotlin/com/launchdarkly/observability/replay/InteractionSource.kt @@ -2,7 +2,7 @@ package com.launchdarkly.observability.replay import android.view.MotionEvent import com.launchdarkly.observability.client.TouchSample -import io.opentelemetry.android.session.SessionManager +import io.opentelemetry.android.session.SessionProvider import kotlinx.coroutines.channels.BufferOverflow import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.SharedFlow @@ -23,7 +23,7 @@ import kotlinx.coroutines.flow.asSharedFlow * @param density the display density used to derive the pixel scale factor. */ class InteractionSource( - private val sessionManager: SessionManager, + private val sessionManager: SessionProvider, private val scale: Double?, private val density: Double, ) { diff --git a/sdk/@launchdarkly/observability-android/lib/src/main/kotlin/com/launchdarkly/observability/replay/SessionReplayService.kt b/sdk/@launchdarkly/observability-android/lib/src/main/kotlin/com/launchdarkly/observability/replay/SessionReplayService.kt index a9239e4323..7658a8eae7 100644 --- a/sdk/@launchdarkly/observability-android/lib/src/main/kotlin/com/launchdarkly/observability/replay/SessionReplayService.kt +++ b/sdk/@launchdarkly/observability-android/lib/src/main/kotlin/com/launchdarkly/observability/replay/SessionReplayService.kt @@ -28,7 +28,7 @@ import com.launchdarkly.observability.replay.transport.EventQueue import com.launchdarkly.observability.context.LDObserveContext import com.launchdarkly.observability.sdk.SessionReplayServicing import com.launchdarkly.observability.util.requireMainThread -import io.opentelemetry.android.session.SessionManager +import io.opentelemetry.android.session.SessionProvider import io.opentelemetry.api.common.Attributes import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Job @@ -77,7 +77,7 @@ class SessionReplayService( private val imageCaptureService: ImageCaptureServicing? = null, ) : SessionReplayServicing { - private lateinit var sessionManager: SessionManager + private lateinit var sessionManager: SessionProvider private val logger: ObserveLogger = observabilityContext.logger private val eventQueue = EventQueue() private val batchWorker = BatchWorker(eventQueue, logger) diff --git a/sdk/@launchdarkly/observability-android/lib/src/main/kotlin/com/launchdarkly/observability/replay/capture/CaptureManager.kt b/sdk/@launchdarkly/observability-android/lib/src/main/kotlin/com/launchdarkly/observability/replay/capture/CaptureManager.kt index 9b7b423bab..2e20f7a6c2 100644 --- a/sdk/@launchdarkly/observability-android/lib/src/main/kotlin/com/launchdarkly/observability/replay/capture/CaptureManager.kt +++ b/sdk/@launchdarkly/observability-android/lib/src/main/kotlin/com/launchdarkly/observability/replay/capture/CaptureManager.kt @@ -2,7 +2,7 @@ package com.launchdarkly.observability.replay.capture import com.launchdarkly.observability.context.ObserveLogger import com.launchdarkly.observability.replay.ReplayOptions -import io.opentelemetry.android.session.SessionManager +import io.opentelemetry.android.session.SessionProvider import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.SharedFlow import kotlinx.coroutines.flow.asSharedFlow @@ -14,7 +14,7 @@ import kotlinx.coroutines.flow.asSharedFlow * @param sessionManager Used to get current session for tagging [ExportFrame] with session id */ class CaptureManager( - private val sessionManager: SessionManager, + private val sessionManager: SessionProvider, private val options: ReplayOptions, private val logger: ObserveLogger, private val imageCaptureService: ImageCaptureServicing = ImageCaptureService(options, logger), diff --git a/sdk/@launchdarkly/observability-android/lib/src/main/kotlin/io/opentelemetry/android/LDRumSessionManagerAccessor.kt b/sdk/@launchdarkly/observability-android/lib/src/main/kotlin/io/opentelemetry/android/LDRumSessionManagerAccessor.kt deleted file mode 100644 index 3ba2a22081..0000000000 --- a/sdk/@launchdarkly/observability-android/lib/src/main/kotlin/io/opentelemetry/android/LDRumSessionManagerAccessor.kt +++ /dev/null @@ -1,17 +0,0 @@ -package io.opentelemetry.android - -import io.opentelemetry.android.session.SessionManager - -/** - * Bridges to [OpenTelemetryRumBuilder.setSessionManager], which is package-private in - * OpenTelemetry Android. Declaring this object in the same `io.opentelemetry.android` package lets - * the LaunchDarkly SDK supply its own [SessionManager] (see - * [com.launchdarkly.observability.client.LDSessionManager]) so it backs the RUM SDK's `session.id` - * span/log appenders, rather than relying on the default manager captured via an instrumentation. - */ -internal object LDRumSessionManagerAccessor { - fun setSessionManager( - builder: OpenTelemetryRumBuilder, - sessionManager: SessionManager, - ): OpenTelemetryRumBuilder = builder.setSessionManager(sessionManager) -} diff --git a/sdk/@launchdarkly/observability-android/lib/src/test/kotlin/com/launchdarkly/observability/replay/InteractionMoveGrouperTest.kt b/sdk/@launchdarkly/observability-android/lib/src/test/kotlin/com/launchdarkly/observability/replay/InteractionMoveGrouperTest.kt index b8dc15f725..1397a78f55 100644 --- a/sdk/@launchdarkly/observability-android/lib/src/test/kotlin/com/launchdarkly/observability/replay/InteractionMoveGrouperTest.kt +++ b/sdk/@launchdarkly/observability-android/lib/src/test/kotlin/com/launchdarkly/observability/replay/InteractionMoveGrouperTest.kt @@ -3,7 +3,7 @@ package com.launchdarkly.observability.replay import android.view.MotionEvent import io.mockk.every import io.mockk.mockk -import io.opentelemetry.android.session.SessionManager +import io.opentelemetry.android.session.SessionProvider import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.take import kotlinx.coroutines.flow.toList @@ -14,13 +14,13 @@ import org.junit.jupiter.api.Test class InteractionMoveGrouperTest { - private lateinit var mockSessionManager: SessionManager + private lateinit var mockSessionManager: SessionProvider private lateinit var bufferedFlow: MutableSharedFlow private lateinit var grouper: InteractionMoveGrouper @BeforeEach fun setUp() { - mockSessionManager = mockk(relaxed = true) + mockSessionManager = mockk(relaxed = true) every { mockSessionManager.getSessionId() } returns "test-session-id" bufferedFlow = MutableSharedFlow(replay = 64, extraBufferCapacity = 64) grouper = InteractionMoveGrouper(mockSessionManager, bufferedFlow) diff --git a/sdk/@launchdarkly/observability-android/lib/src/test/kotlin/com/launchdarkly/observability/replay/SessionReplayTest.kt b/sdk/@launchdarkly/observability-android/lib/src/test/kotlin/com/launchdarkly/observability/replay/SessionReplayTest.kt index 874dabcd20..cb6a061f4d 100644 --- a/sdk/@launchdarkly/observability-android/lib/src/test/kotlin/com/launchdarkly/observability/replay/SessionReplayTest.kt +++ b/sdk/@launchdarkly/observability-android/lib/src/test/kotlin/com/launchdarkly/observability/replay/SessionReplayTest.kt @@ -57,7 +57,7 @@ class SessionReplayTest { @Test fun `initialize wires up LDReplay when service install succeeds`() { // Substitute a stub service for the one register() created so we can decide the - // SessionReplayService.initialize() outcome without standing up a real SessionManager, + // SessionReplayService.initialize() outcome without standing up a real SessionProvider, // ProcessLifecycleOwner, etc. — none of which are available in plain JVM tests. val service = mockk(relaxed = true) every { service.initialize() } returns true diff --git a/sdk/@launchdarkly/observability-android/lib/src/test/kotlin/com/launchdarkly/observability/replay/capture/CaptureManagerTest.kt b/sdk/@launchdarkly/observability-android/lib/src/test/kotlin/com/launchdarkly/observability/replay/capture/CaptureManagerTest.kt index c7ab7323af..ba4ad793f6 100644 --- a/sdk/@launchdarkly/observability-android/lib/src/test/kotlin/com/launchdarkly/observability/replay/capture/CaptureManagerTest.kt +++ b/sdk/@launchdarkly/observability-android/lib/src/test/kotlin/com/launchdarkly/observability/replay/capture/CaptureManagerTest.kt @@ -3,7 +3,7 @@ package com.launchdarkly.observability.replay.capture import com.launchdarkly.observability.context.ObserveLogger import com.launchdarkly.observability.replay.ReplayOptions import io.mockk.mockk -import io.opentelemetry.android.session.SessionManager +import io.opentelemetry.android.session.SessionProvider import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Test @@ -24,7 +24,7 @@ class CaptureManagerTest { } private fun captureManager(options: ReplayOptions = ReplayOptions()) = CaptureManager( - sessionManager = mockk(relaxed = true), + sessionManager = mockk(relaxed = true), options = options, logger = mockk(relaxed = true), imageCaptureService = mockk(relaxed = true), From 4098e02f6df60e37c60ca06364766f4a9a8554dd Mon Sep 17 00:00:00 2001 From: Andrey Belonogov Date: Tue, 6 Oct 2026 09:10:14 -0700 Subject: [PATCH 7/9] chore(maui): bump LaunchDarkly.SessionReplay to 0.11.3 Co-authored-by: Cursor --- .../mobile-dotnet/observability/Directory.Build.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sdk/@launchdarkly/mobile-dotnet/observability/Directory.Build.props b/sdk/@launchdarkly/mobile-dotnet/observability/Directory.Build.props index 18ee46a432..ac83e36c3d 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.2 + 0.11.3 LaunchDarkly LaunchDarkly LaunchDarkly From 43a4eeb4afc5d68bedb990effe720d082793f8c0 Mon Sep 17 00:00:00 2001 From: Andrey Belonogov Date: Tue, 6 Oct 2026 10:44:36 -0700 Subject: [PATCH 8/9] HttpActivityPropagationSupport --- .../mobile-dotnet/.vscode/settings.json | 3 +++ sdk/@launchdarkly/mobile-dotnet/README.md | 14 +++++++++++++- .../mobile-dotnet/sample/MauiSample10.csproj | 7 +++++++ .../mobile-dotnet/sample/MauiSample9.csproj | 7 +++++++ 4 files changed, 30 insertions(+), 1 deletion(-) create mode 100644 sdk/@launchdarkly/mobile-dotnet/.vscode/settings.json 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 42e492c553..39997fe6a8 100644 --- a/sdk/@launchdarkly/mobile-dotnet/README.md +++ b/sdk/@launchdarkly/mobile-dotnet/README.md @@ -11,7 +11,7 @@ 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 made through `HttpClient` (`instrumentation.networkRequests`) +- **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 @@ -23,6 +23,18 @@ Automatic crash reporting is not enabled yet; use `LDObserve.RecordError` to rep * **.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. `MauiSample9.csproj` builds it for .NET 9 and `MauiSample10.csproj` for .NET 10; both share the same sources. diff --git a/sdk/@launchdarkly/mobile-dotnet/sample/MauiSample10.csproj b/sdk/@launchdarkly/mobile-dotnet/sample/MauiSample10.csproj index fe96fd899c..0f51bc7794 100644 --- a/sdk/@launchdarkly/mobile-dotnet/sample/MauiSample10.csproj +++ b/sdk/@launchdarkly/mobile-dotnet/sample/MauiSample10.csproj @@ -136,6 +136,13 @@ true + + + true + + true + true + + true - diff --git a/sdk/@launchdarkly/mobile-dotnet/observability/NativeAndroidDeps.props b/sdk/@launchdarkly/mobile-dotnet/observability/NativeAndroidDeps.props index a77a25c5dd..325b34f8a5 100644 --- a/sdk/@launchdarkly/mobile-dotnet/observability/NativeAndroidDeps.props +++ b/sdk/@launchdarkly/mobile-dotnet/observability/NativeAndroidDeps.props @@ -18,39 +18,20 @@ - - <_LDNativeAAR Include="$(_LDNativeDepsDir)agent-api-*.aar" /> + <_LDNativeAAR Include="$(_LDNativeDepsDir)android-agent-*.aar" /> <_LDNativeAAR Include="$(_LDNativeDepsDir)android-instrumentation-*.aar" /> - - <_LDNativeAAR Include="$(_LDNativeDepsDir)common-1*.aar" /> + <_LDNativeAAR Include="$(_LDNativeDepsDir)common-0*.aar" /> <_LDNativeAAR Include="$(_LDNativeDepsDir)common-api-*.aar" /> <_LDNativeAAR Include="$(_LDNativeDepsDir)core-*-alpha.aar" /> <_LDNativeAAR Include="$(_LDNativeDepsDir)anr-*-alpha.aar" /> <_LDNativeAAR Include="$(_LDNativeDepsDir)activity-*-alpha.aar" /> <_LDNativeAAR Include="$(_LDNativeDepsDir)fragment-*-alpha.aar" /> <_LDNativeAAR Include="$(_LDNativeDepsDir)network-*-alpha.aar" /> - <_LDNativeAAR Include="$(_LDNativeDepsDir)screen-orientation-*-alpha.aar" /> <_LDNativeAAR Include="$(_LDNativeDepsDir)services-*-alpha.aar" /> - - <_LDNativeAAR Include="$(_LDNativeDepsDir)session-*.aar" /> - <_LDNativeAAR Include="$(_LDNativeDepsDir)sessions-*-alpha.aar" /> + <_LDNativeAAR Include="$(_LDNativeDepsDir)session-*-alpha.aar" /> <_LDNativeAAR Include="$(_LDNativeDepsDir)slowrendering-*-alpha.aar" /> <_LDNativeAAR Include="$(_LDNativeDepsDir)startup-*-alpha.aar" /> - - <_LDNativeAAR Include="$(_LDNativeDepsDir)semconv-*-alpha.aar" /> - - <_LDNativeAAR Include="$(_LDNativeDepsDir)semconv.aar" /> <_LDNativeAAR Include="$(_LDNativeDepsDir)lib-release.aar" /> diff --git a/sdk/@launchdarkly/observability-android/lib/build.gradle.kts b/sdk/@launchdarkly/observability-android/lib/build.gradle.kts index e033acb0a6..c3765dcf30 100644 --- a/sdk/@launchdarkly/observability-android/lib/build.gradle.kts +++ b/sdk/@launchdarkly/observability-android/lib/build.gradle.kts @@ -31,7 +31,7 @@ val isClientSdkProvidedByHost = // Pin Kotlin runtime artifacts on this build's classpath to match the configured Kotlin // compiler version (2.0.21). Without this, transitive deps such as the -// io.opentelemetry.android:*:1.7.0 modules drag in newer kotlin-stdlib / kotlin-reflect +// io.opentelemetry.android:*:0.13.0-alpha modules drag in newer kotlin-stdlib / kotlin-reflect // whose metadata version the 2.0.21 compiler cannot read, producing // "Module was compiled with an incompatible version of Kotlin" errors at compileDebugKotlin. // @@ -50,26 +50,6 @@ configurations.all { useVersion("2.0.21") because("Align Kotlin runtime artifacts with the project's Kotlin compiler version (2.0.21).") } - // io.opentelemetry.android:*:1.7.0 pulls androidx.lifecycle 2.11.0, which in turn requires - // kotlinx-coroutines 1.11.0. That release carries Kotlin metadata 2.2.0, and the 2.0.21 - // compiler reads at most 2.1.0. 1.10.2 is the newest release still on metadata 2.1.0. - if (requested.group == "org.jetbrains.kotlinx" && name.startsWith("kotlinx-coroutines")) { - useVersion("1.10.2") - because("kotlinx-coroutines 1.11.0 carries Kotlin 2.2 metadata the 2.0.21 compiler cannot read.") - } - // io.opentelemetry.android:common:1.7.0-alpha pulls androidx.core 1.19.0, which refuses to - // build on anything below AGP 9.1 / compileSdk 37. Hold it at the newest release that still - // builds against compileSdk 35 with AGP 8.x. - if (requested.group == "androidx.core" && (name == "core" || name == "core-ktx")) { - useVersion("1.16.0") - because("androidx.core 1.17.0+ requires a compileSdk/AGP combination this project does not target yet.") - } - // Same story for okhttp: the OTel 1.66 okhttp sender asks for 5.5.0, whose Android variant - // requires compileSdk 37. - if (requested.group == "com.squareup.okhttp3") { - useVersion("5.2.1") - because("okhttp-android 5.3.0+ requires compileSdk 37, beyond what AGP 8.x supports here.") - } } } @@ -93,33 +73,28 @@ dependencies { implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.9.0") // TODO: revise these versions to be as old as usable for compatibility - implementation("io.opentelemetry:opentelemetry-api:1.66.0") - implementation("io.opentelemetry:opentelemetry-sdk:1.66.0") - implementation("io.opentelemetry:opentelemetry-sdk-metrics:1.66.0") - implementation("io.opentelemetry:opentelemetry-sdk-logs:1.66.0") + implementation("io.opentelemetry:opentelemetry-api:1.52.0") + implementation("io.opentelemetry:opentelemetry-sdk:1.52.0") + implementation("io.opentelemetry:opentelemetry-sdk-metrics:1.52.0") + implementation("io.opentelemetry:opentelemetry-sdk-logs:1.52.0") // Required at runtime by io.opentelemetry.android:core, which uses incubator APIs // internally for the logs bridge. Can be removed once the OTel Android SDK drops this dependency. - implementation("io.opentelemetry:opentelemetry-api-incubator:1.66.0-alpha") + implementation("io.opentelemetry:opentelemetry-api-incubator:1.52.0-alpha") // OTEL Android - implementation("io.opentelemetry.android:core:1.7.0-alpha") - implementation("io.opentelemetry.android:session:1.7.0") - - // `OpenTelemetryRum` and `AndroidInstrumentation` moved out of :core in 1.x and are only - // runtime-scoped transitives of it, so they must be declared to be on the compile classpath. - implementation("io.opentelemetry.android:agent-api:1.7.0") - implementation("io.opentelemetry.android.instrumentation:android-instrumentation:1.7.0") + implementation("io.opentelemetry.android:core:0.13.0-alpha") + implementation("io.opentelemetry.android:session:0.13.0-alpha") // OTEL Android Instrumentations - implementation("io.opentelemetry.android.instrumentation:crash:1.7.0-alpha") + implementation("io.opentelemetry.android.instrumentation:crash:0.13.0-alpha") // NOTE: the `activity` instrumentation is intentionally NOT depended on. It is superseded by // LaunchDarkly's own app/screen lifecycle spans and would otherwise double-report; we also // defensively suppress it by name (see ObservabilityService.createOtelRumConfig). // Use JUnit Jupiter for testing. // Testing exporters for telemetry inspection - testImplementation("io.opentelemetry:opentelemetry-sdk-testing:1.66.0") + testImplementation("io.opentelemetry:opentelemetry-sdk-testing:1.52.0") testImplementation(platform("org.junit:junit-bom:5.13.4")) testImplementation("org.junit.jupiter:junit-jupiter") testRuntimeOnly("org.junit.platform:junit-platform-launcher") @@ -129,7 +104,7 @@ dependencies { testImplementation("io.mockk:mockk:1.14.5") testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.9.0") - testFixturesApi("io.opentelemetry:opentelemetry-sdk-testing:1.66.0") + testFixturesApi("io.opentelemetry:opentelemetry-sdk-testing:1.52.0") testFixturesImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.9.0") } diff --git a/sdk/@launchdarkly/observability-android/lib/src/main/kotlin/com/launchdarkly/observability/bridge/LDValueAttributes.kt b/sdk/@launchdarkly/observability-android/lib/src/main/kotlin/com/launchdarkly/observability/bridge/LDValueAttributes.kt index d5d104038b..a2c3daedd7 100644 --- a/sdk/@launchdarkly/observability-android/lib/src/main/kotlin/com/launchdarkly/observability/bridge/LDValueAttributes.kt +++ b/sdk/@launchdarkly/observability-android/lib/src/main/kotlin/com/launchdarkly/observability/bridge/LDValueAttributes.kt @@ -112,5 +112,4 @@ private fun reKey(original: AttributeKey<*>, newName: String): AttributeKey<*> = AttributeType.BOOLEAN_ARRAY -> AttributeKey.booleanArrayKey(newName) AttributeType.LONG_ARRAY -> AttributeKey.longArrayKey(newName) AttributeType.DOUBLE_ARRAY -> AttributeKey.doubleArrayKey(newName) - AttributeType.VALUE -> AttributeKey.valueKey(newName) } diff --git a/sdk/@launchdarkly/observability-android/lib/src/main/kotlin/com/launchdarkly/observability/client/LDSessionManager.kt b/sdk/@launchdarkly/observability-android/lib/src/main/kotlin/com/launchdarkly/observability/client/LDSessionManager.kt index 7c9df01a64..112d6f9761 100644 --- a/sdk/@launchdarkly/observability-android/lib/src/main/kotlin/com/launchdarkly/observability/client/LDSessionManager.kt +++ b/sdk/@launchdarkly/observability-android/lib/src/main/kotlin/com/launchdarkly/observability/client/LDSessionManager.kt @@ -107,7 +107,7 @@ internal class LDSessionManager( observer.onSessionStarted(newSession, previousSession) } } - return newSession.id + return newSession.getId() } /** Marks the app as transitioning to the foreground; the next event settles it to foreground. */ @@ -121,7 +121,7 @@ internal class LDSessionManager( } private fun sessionHasExpired(): Boolean { - val elapsed = clock.now() - session.startTimestamp + val elapsed = clock.now() - session.getStartTimestamp() return elapsed >= maxLifetime.inWholeNanoseconds } @@ -149,9 +149,13 @@ internal class LDSessionManager( } private class LDSession( - override val id: String, - override val startTimestamp: Long, - ) : Session + private val id: String, + private val startTimestamp: Long, + ) : Session { + override fun getId(): String = id + + override fun getStartTimestamp(): Long = startTimestamp + } private companion object { /** diff --git a/sdk/@launchdarkly/observability-android/lib/src/main/kotlin/com/launchdarkly/observability/client/LaunchTimeInstrumentation.kt b/sdk/@launchdarkly/observability-android/lib/src/main/kotlin/com/launchdarkly/observability/client/LaunchTimeInstrumentation.kt index 687fc0e0af..8d0be0a959 100644 --- a/sdk/@launchdarkly/observability-android/lib/src/main/kotlin/com/launchdarkly/observability/client/LaunchTimeInstrumentation.kt +++ b/sdk/@launchdarkly/observability-android/lib/src/main/kotlin/com/launchdarkly/observability/client/LaunchTimeInstrumentation.kt @@ -11,8 +11,8 @@ import android.view.View import android.view.ViewTreeObserver.OnDrawListener import androidx.activity.FullyDrawnReporterOwner import com.launchdarkly.observability.interfaces.Metric -import io.opentelemetry.android.OpenTelemetryRum import io.opentelemetry.android.instrumentation.AndroidInstrumentation +import io.opentelemetry.android.instrumentation.InstallationContext import io.opentelemetry.api.common.Attributes import java.lang.ref.WeakReference import java.util.WeakHashMap @@ -44,7 +44,7 @@ internal class LaunchTimeInstrumentation( override val name: String = INSTRUMENTATION_SCOPE_NAME - override fun install(context: android.content.Context, rum: OpenTelemetryRum) { + override fun install(ctx: InstallationContext) { application.registerActivityLifecycleCallbacks(this) } diff --git a/sdk/@launchdarkly/observability-android/lib/src/main/kotlin/com/launchdarkly/observability/client/ObservabilityService.kt b/sdk/@launchdarkly/observability-android/lib/src/main/kotlin/com/launchdarkly/observability/client/ObservabilityService.kt index 78e64d3857..65fa62bda9 100644 --- a/sdk/@launchdarkly/observability-android/lib/src/main/kotlin/com/launchdarkly/observability/client/ObservabilityService.kt +++ b/sdk/@launchdarkly/observability-android/lib/src/main/kotlin/com/launchdarkly/observability/client/ObservabilityService.kt @@ -37,7 +37,6 @@ import com.launchdarkly.observability.traces.OtlpTraceExporter import com.launchdarkly.observability.util.requireMainThread import io.opentelemetry.android.OpenTelemetryRum import io.opentelemetry.android.OpenTelemetryRumBuilder -import io.opentelemetry.android.RumBuilder import io.opentelemetry.android.config.OtelRumConfig import io.opentelemetry.android.session.Session import io.opentelemetry.android.session.SessionObserver @@ -243,7 +242,7 @@ class ObservabilityService( } val otelRumConfig = createOtelRumConfig() - val rumBuilder = RumBuilder.builder(application, otelRumConfig) + val rumBuilder = OpenTelemetryRum.builder(application, otelRumConfig) .addLoggerProviderCustomizer { sdkLoggerProviderBuilder, _ -> return@addLoggerProviderCustomizer configureLoggerProvider(sdkLoggerProviderBuilder) } @@ -269,12 +268,12 @@ class ObservabilityService( // previous_screen against the prior session, and a re-appearing first screen would be // deduped instead of emitting a fresh navigation. // - // Only reset on an actual session *change*. The initial session start carries the "none" - // session (empty id) as the previous session; resetting on it would clobber a first + // Only reset on an actual session *change*. The initial session start carries Session.NONE + // (empty id) as the previous session; resetting on it would clobber a first // screen that may already have been recorded by the time this notification fires. ldSessionManager.addObserver(object : SessionObserver { override fun onSessionStarted(newSession: Session, previousSession: Session) { - if (previousSession.id.isNotEmpty()) { + if (previousSession.getId().isNotEmpty()) { screenStack.reset() // Re-seed the new session with the screen the user is still viewing. No // onActivityResumed fires for an already-resumed activity, so without this the diff --git a/sdk/@launchdarkly/observability-android/lib/src/main/kotlin/com/launchdarkly/observability/otlp/json/OtlpJsonEncoding.kt b/sdk/@launchdarkly/observability-android/lib/src/main/kotlin/com/launchdarkly/observability/otlp/json/OtlpJsonEncoding.kt index d7ece34fb4..0ad96510a8 100644 --- a/sdk/@launchdarkly/observability-android/lib/src/main/kotlin/com/launchdarkly/observability/otlp/json/OtlpJsonEncoding.kt +++ b/sdk/@launchdarkly/observability-android/lib/src/main/kotlin/com/launchdarkly/observability/otlp/json/OtlpJsonEncoding.kt @@ -112,7 +112,6 @@ private fun JsonByteWriter.write(value: OtlpJsonAnyValue) { endObject() } is OtlpJsonAnyValue.BytesVal -> name("bytesValue").value(value.value) - is OtlpJsonAnyValue.EmptyVal -> Unit } endObject() } diff --git a/sdk/@launchdarkly/observability-android/lib/src/main/kotlin/com/launchdarkly/observability/otlp/json/common/JsonCommonAdapter.kt b/sdk/@launchdarkly/observability-android/lib/src/main/kotlin/com/launchdarkly/observability/otlp/json/common/JsonCommonAdapter.kt index bf790574fc..4e4db6e31c 100644 --- a/sdk/@launchdarkly/observability-android/lib/src/main/kotlin/com/launchdarkly/observability/otlp/json/common/JsonCommonAdapter.kt +++ b/sdk/@launchdarkly/observability-android/lib/src/main/kotlin/com/launchdarkly/observability/otlp/json/common/JsonCommonAdapter.kt @@ -68,8 +68,6 @@ object JsonCommonAdapter { @Suppress("UNCHECKED_CAST") (value as List).map { OtlpJsonAnyValue.double(it) } ) - // Extended attributes carry a nested Value, which the Value overload already handles. - AttributeType.VALUE -> toJsonAnyValue(value as Value<*>) } /** @@ -101,6 +99,5 @@ object JsonCommonAdapter { buffer.asReadOnlyBuffer().get(bytes) OtlpJsonAnyValue.bytes(Base64.getEncoder().encodeToString(bytes)) } - ValueType.EMPTY -> OtlpJsonAnyValue.empty() } } diff --git a/sdk/@launchdarkly/observability-android/lib/src/main/kotlin/com/launchdarkly/observability/otlp/json/common/OtlpJsonCommonModels.kt b/sdk/@launchdarkly/observability-android/lib/src/main/kotlin/com/launchdarkly/observability/otlp/json/common/OtlpJsonCommonModels.kt index c6f30a3bb8..300f407c78 100644 --- a/sdk/@launchdarkly/observability-android/lib/src/main/kotlin/com/launchdarkly/observability/otlp/json/common/OtlpJsonCommonModels.kt +++ b/sdk/@launchdarkly/observability-android/lib/src/main/kotlin/com/launchdarkly/observability/otlp/json/common/OtlpJsonCommonModels.kt @@ -59,9 +59,6 @@ sealed class OtlpJsonAnyValue { data class BytesVal(val value: String) : OtlpJsonAnyValue() - /** An unset value, written as `{}` (proto `AnyValue` with no `oneof` member set). */ - object EmptyVal : OtlpJsonAnyValue() - data class ArrayWrapper(val values: List = emptyList()) data class KvListWrapper(val values: List = emptyList()) @@ -74,6 +71,5 @@ sealed class OtlpJsonAnyValue { fun array(values: List): OtlpJsonAnyValue = ArrayVal(ArrayWrapper(values)) fun kvlist(values: List): OtlpJsonAnyValue = KvListVal(KvListWrapper(values)) fun bytes(base64: String): OtlpJsonAnyValue = BytesVal(base64) - fun empty(): OtlpJsonAnyValue = EmptyVal } }