From bb8b8e3119f614c31cd44e4a9145a0ff595b89ff Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Wed, 12 Aug 2026 21:54:37 +0000 Subject: [PATCH] fix: [SDK-5006] shade OpenTelemetry to isolate host R8 Relocate io.opentelemetry into com.onesignal.shaded.opentelemetry and embed it in the otel AAR so Gradle cannot unify OneSignal's 1.55 copy with a host BOM (e.g. Embrace 1.64). Drop io.opentelemetry consumer dontwarn rules that leaked into every library in the app. Co-authored-by: abdulraqeeb33 --- .github/workflows/ci.yml | 6 +- .../debug/internal/crash/OtelAnrDetector.kt | 2 +- .../internal/crash/OtelAnrDetectorTest.kt | 2 + OneSignalSDK/onesignal/otel/build.gradle | 223 +++++++++++++++++- .../onesignal/otel/consumer-rules.pro | 27 ++- .../com/onesignal/otel/IOtelOpenTelemetry.kt | 30 +-- .../com/onesignal/otel/IOtelSdkTelemetry.kt | 32 +++ .../onesignal/otel/OneSignalOpenTelemetry.kt | 6 +- .../java/com/onesignal/otel/OtelFactory.kt | 3 + .../com/onesignal/otel/OtelLoggingHelper.kt | 2 +- .../onesignal/otel/crash/OtelCrashReporter.kt | 4 +- .../onesignal/otel/crash/OtelCrashUploader.kt | 3 +- .../otel/OneSignalOpenTelemetryTest.kt | 10 +- .../com/onesignal/otel/OtelFactoryTest.kt | 1 + .../onesignal/otel/OtelLoggingHelperTest.kt | 2 +- .../otel/crash/OtelCrashReporterTest.kt | 4 +- .../otel/crash/OtelCrashUploaderTest.kt | 4 +- OneSignalSDK/settings.gradle | 8 + examples/demo/app/build.gradle.kts | 25 ++ examples/demo/app/proguard-rules.pro | 5 +- 20 files changed, 337 insertions(+), 62 deletions(-) create mode 100644 OneSignalSDK/onesignal/otel/src/main/java/com/onesignal/otel/IOtelSdkTelemetry.kt diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 67fe01bbc0..b31d84c35b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -173,7 +173,11 @@ jobs: - name: "[Build] Demo app (minified GMS + Huawei release)" working-directory: OneSignalSDK run: | - ./gradlew :app:assembleGmsRelease :app:assembleHuaweiRelease --console=plain + ./gradlew :OneSignal:otel:checkOtelIsolation :app:assembleGmsRelease :app:assembleHuaweiRelease --console=plain + - name: "[Build] Demo app with host OpenTelemetry 1.64 (SDK-5006 clash regression)" + working-directory: OneSignalSDK + run: | + ./gradlew :app:assembleGmsRelease -PsimulateOtelClash --console=plain # Keep the existing required check stable while the workloads run independently. build: diff --git a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/debug/internal/crash/OtelAnrDetector.kt b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/debug/internal/crash/OtelAnrDetector.kt index a5733a9095..f5fdf9dbb6 100644 --- a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/debug/internal/crash/OtelAnrDetector.kt +++ b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/debug/internal/crash/OtelAnrDetector.kt @@ -44,8 +44,8 @@ internal class OtelAnrDetector( // Android touch points (main-thread Handler, stack capture) plus the monotonic clock, injectable // so the whole watchdog runs deterministically off-device. private val platform: AnrWatchdogPlatform = AndroidAnrWatchdogPlatform(), + private val crashReporter: IOtelCrashReporter = OtelFactory.createCrashReporter(openTelemetryCrash, logger), ) : IOtelAnrDetector { - private val crashReporter: IOtelCrashReporter = OtelFactory.createCrashReporter(openTelemetryCrash, logger) private val isMonitoring = AtomicBoolean(false) private val evaluator = AnrCheckEvaluator( diff --git a/OneSignalSDK/onesignal/core/src/test/java/com/onesignal/debug/internal/crash/OtelAnrDetectorTest.kt b/OneSignalSDK/onesignal/core/src/test/java/com/onesignal/debug/internal/crash/OtelAnrDetectorTest.kt index 41bd0d51f1..019a0291d8 100644 --- a/OneSignalSDK/onesignal/core/src/test/java/com/onesignal/debug/internal/crash/OtelAnrDetectorTest.kt +++ b/OneSignalSDK/onesignal/core/src/test/java/com/onesignal/debug/internal/crash/OtelAnrDetectorTest.kt @@ -1,5 +1,6 @@ package com.onesignal.debug.internal.crash +import com.onesignal.otel.IOtelCrashReporter import com.onesignal.otel.IOtelLogger import com.onesignal.otel.IOtelOpenTelemetryCrash import com.onesignal.otel.crash.IOtelAnrDetector @@ -63,6 +64,7 @@ class OtelAnrDetectorTest : FunSpec({ backgroundThresholdMs = 10_000L, isAppInForeground = inForeground, platform = platform, + crashReporter = mockk(relaxed = true), ) test("OtelAnrDetector implements IOtelAnrDetector") { diff --git a/OneSignalSDK/onesignal/otel/build.gradle b/OneSignalSDK/onesignal/otel/build.gradle index f9b5d00da8..0127fdb5ef 100644 --- a/OneSignalSDK/onesignal/otel/build.gradle +++ b/OneSignalSDK/onesignal/otel/build.gradle @@ -1,3 +1,14 @@ +buildscript { + repositories { + google() + mavenCentral() + gradlePluginPortal() + } + dependencies { + classpath 'com.gradleup.shadow:shadow-gradle-plugin:8.3.6' + } +} + plugins { id 'com.android.library' id 'kotlin-android' @@ -6,6 +17,8 @@ plugins { id 'io.gitlab.arturbosch.detekt' } +import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar + android { namespace 'com.onesignal.otel' compileSdkVersion rootProject.buildVersions.compileSdkVersion @@ -45,16 +58,49 @@ ext { projectDescription = "OneSignal Android SDK - OpenTelemetry Module" } +configurations { + otelShade { + canBeConsumed = false + canBeResolved = true + visible = false + // Shared host libraries stay as normal Maven transitives so we do not + // duplicate OkHttp / Kotlin / annotations inside the AAR. + exclude group: 'com.squareup.okhttp3' + exclude group: 'com.squareup.okio' + exclude group: 'org.jetbrains.kotlin' + exclude group: 'org.jetbrains.kotlinx' + exclude group: 'androidx.annotation' + exclude group: 'org.jetbrains', module: 'annotations' + exclude group: 'com.google.code.findbugs' + exclude group: 'org.codehaus.mojo' + exclude group: 'com.google.guava' + exclude group: 'com.google.errorprone' + exclude group: 'com.google.auto.value' + exclude group: 'com.fasterxml.jackson.core' + exclude group: 'com.google.code.gson' + exclude group: 'javax.annotation' + exclude group: 'org.checkerframework' + exclude group: 'io.grpc' + exclude group: 'io.netty' + } +} + +def addOtelDependencies(String configurationName) { + dependencies.add(configurationName, dependencies.platform("io.opentelemetry:opentelemetry-bom:$rootProject.opentelemetryBomVersion")) + ['opentelemetry-api', 'opentelemetry-sdk', 'opentelemetry-exporter-otlp'].each { artifact -> + dependencies.add(configurationName, "io.opentelemetry:$artifact") + } + dependencies.add(configurationName, "io.opentelemetry.semconv:opentelemetry-semconv:$rootProject.opentelemetrySemconvVersion") + dependencies.add(configurationName, "io.opentelemetry.contrib:opentelemetry-disk-buffering:$rootProject.opentelemetryDiskBufferingVersion") +} + dependencies { implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlinVersion" implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutinesVersion" - implementation platform("io.opentelemetry:opentelemetry-bom:$rootProject.opentelemetryBomVersion") - implementation('io.opentelemetry:opentelemetry-api') - implementation('io.opentelemetry:opentelemetry-sdk') - implementation('io.opentelemetry:opentelemetry-exporter-otlp') - implementation("io.opentelemetry.semconv:opentelemetry-semconv:$rootProject.opentelemetrySemconvVersion") - implementation("io.opentelemetry.contrib:opentelemetry-disk-buffering:$rootProject.opentelemetryDiskBufferingVersion") + // OTLP HTTP export talks to OkHttp by class name. Keep OkHttp as a normal + // transitive so the host app can share a single copy; do not relocate it. + implementation 'com.squareup.okhttp3:okhttp:4.12.0' testImplementation(project(':OneSignal:testhelpers')) testImplementation("io.kotest:kotest-runner-junit5:$kotestVersion") @@ -65,6 +111,171 @@ dependencies { testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:$coroutinesVersion") } +// Compile against upstream OpenTelemetry. The release AAR embeds a relocated +// copy instead of publishing these as Maven transitives (SDK-5006 / #2714). +addOtelDependencies('compileOnly') +addOtelDependencies('otelShade') + +// Unit tests and non-published variants keep unshaded OpenTelemetry on the +// runtime classpath so existing io.opentelemetry test code keeps compiling. +addOtelDependencies('debugImplementation') +addOtelDependencies('originalImplementation') +addOtelDependencies('unityImplementation') +addOtelDependencies('testImplementation') + +/** + * Relocate OpenTelemetry into a OneSignal-private package and embed it in the + * published release AAR (SDK-5006 / #2714). + * + * Host apps (and other SDKs such as Embrace) keep their own `io.opentelemetry` + * artifacts. Gradle will no longer unify those with OneSignal's copy, so a newer + * BOM cannot delete classes that `opentelemetry-disk-buffering` still references. + * Consumer R8 also never sees OneSignal's `io.opentelemetry` types, so we do not + * ship `-dontwarn io.opentelemetry.**` rules that would suppress diagnostics for + * everyone else in the app. + * + * Debug / original / unity variants keep unshaded OpenTelemetry on the runtime + * classpath so `testDebugUnitTest` can still compile against `io.opentelemetry`. + */ +afterEvaluate { + def kotlinCompile = tasks.named('compileReleaseKotlin') + def javaCompile = tasks.named('compileReleaseJavaWithJavac') + def shadowTask = tasks.register('relocateReleaseOtel', ShadowJar) { + group = 'onesignal' + description = 'Relocate OpenTelemetry packages for the release AAR' + dependsOn kotlinCompile, javaCompile + duplicatesStrategy = DuplicatesStrategy.INCLUDE + zip64 = true + mergeServiceFiles() + configurations = [project.configurations.otelShade] + relocate 'io.opentelemetry', 'com.onesignal.shaded.opentelemetry' + exclude 'META-INF/MANIFEST.MF' + exclude 'META-INF/*.SF' + exclude 'META-INF/*.DSA' + exclude 'META-INF/*.RSA' + exclude 'module-info.class' + exclude 'META-INF/versions/**/module-info.class' + exclude 'META-INF/maven/**' + // OneSignal only uses OTLP/HTTP. Dropping the unused gRPC exporter path + // means consumer R8 never sees io.grpc / Guava-future references, so we + // do not ship host-wide -dontwarn io.grpc.** rules (SDK-5006). + exclude 'io/opentelemetry/exporter/internal/grpc/**' + exclude 'io/opentelemetry/exporter/otlp/**/OtlpGrpc*.class' + exclude 'io/opentelemetry/exporter/otlp/**/Marshaler*Grpc*.class' + exclude 'io/opentelemetry/exporter/otlp/internal/OtlpGrpc*.class' + exclude 'io/opentelemetry/exporter/sender/okhttp/internal/OkHttpGrpc*.class' + exclude 'META-INF/services/io.opentelemetry.exporter.internal.grpc.GrpcSenderProvider' + from kotlinCompile + from javaCompile.map { it.destinationDirectory } + archiveFileName.set('classes.jar') + destinationDirectory.set(layout.buildDirectory.dir('shaded/release')) + } + + def installShadedClasses = { task -> + task.dependsOn shadowTask + task.doLast { + def shadedJar = shadowTask.get().archiveFile.get().asFile + def outputs = task.outputs.files.files + outputs.each { out -> + if (out.isFile()) { + copy { + from shadedJar + into out.parentFile + rename { out.name } + } + } else if (out.isDirectory()) { + delete out + copy { + from zipTree(shadedJar) + into out + } + } + } + } + } + + tasks.named('bundleLibRuntimeToJarRelease').configure { installShadedClasses(it) } + tasks.named('bundleLibRuntimeToDirRelease').configure { installShadedClasses(it) } + + tasks.named('bundleReleaseAar').configure { bundle -> + bundle.dependsOn shadowTask + bundle.doLast { + def aar = bundle.archiveFile.get().asFile + def shadedJar = shadowTask.get().archiveFile.get().asFile + def unpackDir = file("${bundle.temporaryDir}/aar-unpack") + def tmpAar = file("${bundle.temporaryDir}/otel-release-shaded.aar") + delete unpackDir, tmpAar + copy { + from zipTree(aar) + into unpackDir + } + copy { + from shadedJar + into unpackDir + rename { 'classes.jar' } + } + ant.zip(destfile: tmpAar, basedir: unpackDir) + ant.copy(file: tmpAar, tofile: aar, overwrite: true) + } + } +} + +tasks.register('checkOtelIsolation') { + group = 'verification' + description = 'Assert the release AAR embeds relocated OpenTelemetry and does not leak io.opentelemetry Maven artifacts' + dependsOn 'bundleReleaseAar' + doLast { + def leaked = configurations.releaseRuntimeClasspath + .resolvedConfiguration + .resolvedArtifacts + .findAll { it.moduleVersion.id.group.startsWith('io.opentelemetry') } + .collect { it.moduleVersion.id.toString() } + if (!leaked.isEmpty()) { + throw new GradleException( + "otel releaseRuntimeClasspath still leaks OpenTelemetry artifacts (SDK-5006): ${leaked}" + ) + } + + def aarDir = file("${layout.buildDirectory.get()}/outputs/aar") + def aar = file("${aarDir}/otel-release.aar") + if (!aar.exists()) { + def matches = fileTree(aarDir) { include '*.aar' }.files + if (matches.size() == 1) { + aar = matches.first() + } else { + throw new GradleException("Could not find otel-release.aar under ${aarDir}") + } + } + + def unzipDir = file("${layout.buildDirectory.get()}/otel-isolation-check") + delete unzipDir + copy { + from zipTree(aar) + into unzipDir + } + def classesJar = file("${unzipDir}/classes.jar") + if (!classesJar.exists()) { + throw new GradleException("otel-release.aar is missing classes.jar") + } + + def classFiles = zipTree(classesJar).matching { include '**/*.class' }.files.collect { it.path.replace(File.separator, '/') } + def hasShaded = classFiles.any { it.contains('/com/onesignal/shaded/opentelemetry/') } + def hasUnshaded = classFiles.any { it.contains('/io/opentelemetry/') } + if (!hasShaded) { + throw new GradleException('Expected relocated com.onesignal.shaded.opentelemetry classes in otel-release.aar') + } + if (hasUnshaded) { + throw new GradleException('Unshaded io.opentelemetry classes leaked into otel-release.aar') + } + } +} + +afterEvaluate { + tasks.named('check').configure { + dependsOn 'checkOtelIsolation' + } +} + apply from: '../detekt.gradle' apply from: '../spotless.gradle' apply from: '../maven-push.gradle' diff --git a/OneSignalSDK/onesignal/otel/consumer-rules.pro b/OneSignalSDK/onesignal/otel/consumer-rules.pro index 76b04edace..2ac9713cf4 100644 --- a/OneSignalSDK/onesignal/otel/consumer-rules.pro +++ b/OneSignalSDK/onesignal/otel/consumer-rules.pro @@ -1,17 +1,20 @@ -# OpenTelemetry OTLP exporter references Jackson core classes that are optional on Android. -# Suppress R8 missing-class errors when apps don't include jackson-core. --dontwarn com.fasterxml.jackson.core.** +# OpenTelemetry OTLP exporter references Jackson classes that are optional on Android. +# These match the MISSING jackson/autovalue types (not io.opentelemetry), so they do not +# hide OpenTelemetry diagnostics from other libraries in the host app. +-dontwarn com.fasterxml.jackson.** # OTel (e.g. sdk-logs AutoValue-generated types) references Google Auto Value annotations that are # not on the app classpath. Wildcard covers inner types and extensions (e.g. Memoized). -dontwarn com.google.auto.value.** -# Suppress R8 missing-class errors from OpenTelemetry version skew. When a host app bumps the -# transitive opentelemetry-bom, removed internal classes (e.g. io.opentelemetry.api.internal.ApiUsageLogger) -# leave dangling references from the unused opentelemetry-api-incubator alpha (ExtendedDefaultTracer). -# R8 suppresses a "Missing class" diagnostic when the MISSING class matches -dontwarn, so match the -# io.opentelemetry.api.internal package directly (referrer-independent). Scoped to api.internal (not a -# broad **.internal.** wildcard) so genuine missing-class errors in the sdk/exporter internals that -# OneSignal actively uses still surface. The incubator rule covers the unused ExtendedDefaultTracer path. --dontwarn io.opentelemetry.api.incubator.** --dontwarn io.opentelemetry.api.internal.** +# OpenTelemetry itself is relocated into com.onesignal.shaded.opentelemetry and embedded in this +# AAR (SDK-5006 / #2714). Do NOT add -dontwarn io.opentelemetry.** here: consumer rules are merged +# into the host app's R8 config and would suppress missing-class errors for every other library +# that uses OpenTelemetry (e.g. Embrace). Matching the relocated package keeps remaining +# optional-path suppressions (incubator config, unused exporters) OneSignal-private. +-keep class com.onesignal.shaded.opentelemetry.** { *; } +-dontwarn com.onesignal.shaded.opentelemetry.** + +# jctools package-info inside the relocated SDK still names OSGi bundle annotations +# that are not on the Android classpath. Scoped to that annotation package only. +-dontwarn org.osgi.annotation.bundle.** diff --git a/OneSignalSDK/onesignal/otel/src/main/java/com/onesignal/otel/IOtelOpenTelemetry.kt b/OneSignalSDK/onesignal/otel/src/main/java/com/onesignal/otel/IOtelOpenTelemetry.kt index 156df29ffd..8f2395b85e 100644 --- a/OneSignalSDK/onesignal/otel/src/main/java/com/onesignal/otel/IOtelOpenTelemetry.kt +++ b/OneSignalSDK/onesignal/otel/src/main/java/com/onesignal/otel/IOtelOpenTelemetry.kt @@ -1,29 +1,13 @@ package com.onesignal.otel -import io.opentelemetry.api.logs.LogRecordBuilder -import io.opentelemetry.sdk.common.CompletableResultCode -import io.opentelemetry.sdk.logs.export.LogRecordExporter - /** - * Platform-agnostic OpenTelemetry interface. + * Platform-agnostic OpenTelemetry handle. + * + * OpenTelemetry Java types are intentionally absent from this public surface so the + * `:otel` module can relocate `io.opentelemetry` into a private package (SDK-5006). + * Host apps and `:core` must not compile against those types. */ interface IOtelOpenTelemetry { - /** - * Gets a LogRecordBuilder for creating log records. - * This is a suspend function as it may need to initialize the SDK on first call. - * - * @return A LogRecordBuilder instance for building log records - */ - suspend fun getLogger(): LogRecordBuilder - - /** - * Forces a flush of all pending log records. - * This ensures all buffered logs are exported immediately. - * - * @return A CompletableResultCode indicating the flush operation result - */ - suspend fun forceFlush(): CompletableResultCode - /** * Shuts down the underlying OpenTelemetry SDK, flushing pending data * and releasing resources (exporters, logger providers, etc.). @@ -40,6 +24,4 @@ interface IOtelOpenTelemetryCrash : IOtelOpenTelemetry /** * Interface for remote OpenTelemetry (network export). */ -interface IOtelOpenTelemetryRemote : IOtelOpenTelemetry { - val logExporter: LogRecordExporter -} +interface IOtelOpenTelemetryRemote : IOtelOpenTelemetry diff --git a/OneSignalSDK/onesignal/otel/src/main/java/com/onesignal/otel/IOtelSdkTelemetry.kt b/OneSignalSDK/onesignal/otel/src/main/java/com/onesignal/otel/IOtelSdkTelemetry.kt new file mode 100644 index 0000000000..f97e830f82 --- /dev/null +++ b/OneSignalSDK/onesignal/otel/src/main/java/com/onesignal/otel/IOtelSdkTelemetry.kt @@ -0,0 +1,32 @@ +package com.onesignal.otel + +import io.opentelemetry.api.logs.LogRecordBuilder +import io.opentelemetry.sdk.common.CompletableResultCode +import io.opentelemetry.sdk.logs.export.LogRecordExporter + +/** + * OpenTelemetry SDK surface used only inside `:otel`. + * + * Kept `internal` so `:core` and host apps never compile against `io.opentelemetry` + * types. That boundary is what lets the release AAR relocate those packages without + * leaking them onto the consumer compile/runtime classpath (SDK-5006). + */ +internal interface IOtelSdkTelemetry : IOtelOpenTelemetry { + /** + * Gets a LogRecordBuilder for creating log records. + * This is a suspend function as it may need to initialize the SDK on first call. + */ + suspend fun getLogger(): LogRecordBuilder + + /** + * Forces a flush of all pending log records. + * This ensures all buffered logs are exported immediately. + */ + suspend fun forceFlush(): CompletableResultCode +} + +internal interface IOtelSdkCrashTelemetry : IOtelOpenTelemetryCrash, IOtelSdkTelemetry + +internal interface IOtelSdkRemoteTelemetry : IOtelOpenTelemetryRemote, IOtelSdkTelemetry { + val logExporter: LogRecordExporter +} diff --git a/OneSignalSDK/onesignal/otel/src/main/java/com/onesignal/otel/OneSignalOpenTelemetry.kt b/OneSignalSDK/onesignal/otel/src/main/java/com/onesignal/otel/OneSignalOpenTelemetry.kt index ea66980ab3..03e488ac58 100644 --- a/OneSignalSDK/onesignal/otel/src/main/java/com/onesignal/otel/OneSignalOpenTelemetry.kt +++ b/OneSignalSDK/onesignal/otel/src/main/java/com/onesignal/otel/OneSignalOpenTelemetry.kt @@ -20,7 +20,7 @@ internal fun LogRecordBuilder.setAllAttributes(attributes: Map): internal abstract class OneSignalOpenTelemetryBase( private val osTopLevelFields: OtelFieldsTopLevel, private val osPerEventFields: OtelFieldsPerEvent, -) : IOtelOpenTelemetry { +) : IOtelSdkTelemetry { private val lock = Any() private var sdkCachedValue: OpenTelemetrySdk? = null @@ -79,7 +79,7 @@ internal class OneSignalOpenTelemetryRemote( osTopLevelFields: OtelFieldsTopLevel, osPerEventFields: OtelFieldsPerEvent, ) : OneSignalOpenTelemetryBase(osTopLevelFields, osPerEventFields), - IOtelOpenTelemetryRemote { + IOtelSdkRemoteTelemetry { private val appId: String get() = platformProvider.appIdForHeaders @@ -119,7 +119,7 @@ internal class OneSignalOpenTelemetryCrashLocal( osTopLevelFields: OtelFieldsTopLevel, osPerEventFields: OtelFieldsPerEvent, ) : OneSignalOpenTelemetryBase(osTopLevelFields, osPerEventFields), - IOtelOpenTelemetryCrash { + IOtelSdkCrashTelemetry { override fun getSdkInstance(attributes: Map): OpenTelemetrySdk = OpenTelemetrySdk .builder() diff --git a/OneSignalSDK/onesignal/otel/src/main/java/com/onesignal/otel/OtelFactory.kt b/OneSignalSDK/onesignal/otel/src/main/java/com/onesignal/otel/OtelFactory.kt index c4e46e6630..cc3be73e39 100644 --- a/OneSignalSDK/onesignal/otel/src/main/java/com/onesignal/otel/OtelFactory.kt +++ b/OneSignalSDK/onesignal/otel/src/main/java/com/onesignal/otel/OtelFactory.kt @@ -107,6 +107,9 @@ object OtelFactory { openTelemetryCrash: IOtelOpenTelemetryCrash, logger: IOtelLogger, ): IOtelCrashReporter { + require(openTelemetryCrash is IOtelSdkTelemetry) { + "openTelemetryCrash must be a OneSignal-created telemetry instance" + } return OtelCrashReporter(openTelemetryCrash, logger) } } diff --git a/OneSignalSDK/onesignal/otel/src/main/java/com/onesignal/otel/OtelLoggingHelper.kt b/OneSignalSDK/onesignal/otel/src/main/java/com/onesignal/otel/OtelLoggingHelper.kt index 8b1c85c7b0..03b9ad1a16 100644 --- a/OneSignalSDK/onesignal/otel/src/main/java/com/onesignal/otel/OtelLoggingHelper.kt +++ b/OneSignalSDK/onesignal/otel/src/main/java/com/onesignal/otel/OtelLoggingHelper.kt @@ -55,7 +55,7 @@ object OtelLoggingHelper { } .build() - val logRecordBuilder = telemetry.getLogger() + val logRecordBuilder = (telemetry as IOtelSdkTelemetry).getLogger() logRecordBuilder.setAllAttributes(attributes) logRecordBuilder.setSeverity(severity) logRecordBuilder.setBody(message) diff --git a/OneSignalSDK/onesignal/otel/src/main/java/com/onesignal/otel/crash/OtelCrashReporter.kt b/OneSignalSDK/onesignal/otel/src/main/java/com/onesignal/otel/crash/OtelCrashReporter.kt index 44972ebdd0..fd91d13b9b 100644 --- a/OneSignalSDK/onesignal/otel/src/main/java/com/onesignal/otel/crash/OtelCrashReporter.kt +++ b/OneSignalSDK/onesignal/otel/src/main/java/com/onesignal/otel/crash/OtelCrashReporter.kt @@ -1,13 +1,13 @@ package com.onesignal.otel.crash import com.onesignal.otel.IOtelLogger -import com.onesignal.otel.IOtelOpenTelemetryCrash +import com.onesignal.otel.IOtelSdkTelemetry import io.opentelemetry.api.common.Attributes import io.opentelemetry.api.logs.Severity import java.time.Instant internal class OtelCrashReporter( - private val openTelemetry: IOtelOpenTelemetryCrash, + private val openTelemetry: IOtelSdkTelemetry, private val logger: IOtelLogger, ) : com.onesignal.otel.IOtelCrashReporter { companion object { diff --git a/OneSignalSDK/onesignal/otel/src/main/java/com/onesignal/otel/crash/OtelCrashUploader.kt b/OneSignalSDK/onesignal/otel/src/main/java/com/onesignal/otel/crash/OtelCrashUploader.kt index 31f1573e90..09dd0340c2 100644 --- a/OneSignalSDK/onesignal/otel/src/main/java/com/onesignal/otel/crash/OtelCrashUploader.kt +++ b/OneSignalSDK/onesignal/otel/src/main/java/com/onesignal/otel/crash/OtelCrashUploader.kt @@ -3,6 +3,7 @@ package com.onesignal.otel.crash import com.onesignal.otel.IOtelLogger import com.onesignal.otel.IOtelOpenTelemetryRemote import com.onesignal.otel.IOtelPlatformProvider +import com.onesignal.otel.IOtelSdkRemoteTelemetry import com.onesignal.otel.config.OtelConfigCrashFile import io.opentelemetry.sdk.logs.data.LogRecordData import kotlinx.coroutines.delay @@ -85,7 +86,7 @@ class OtelCrashUploader( } internal fun sendCrashReports(reports: Iterator>) { - val networkExporter = openTelemetryRemote.logExporter + val networkExporter = (openTelemetryRemote as IOtelSdkRemoteTelemetry).logExporter var failed = false var sentBatches = 0 // NOTE: next() will delete the previous report, so we only want to send diff --git a/OneSignalSDK/onesignal/otel/src/test/java/com/onesignal/otel/OneSignalOpenTelemetryTest.kt b/OneSignalSDK/onesignal/otel/src/test/java/com/onesignal/otel/OneSignalOpenTelemetryTest.kt index 775c1ad047..b1fb6b36cc 100644 --- a/OneSignalSDK/onesignal/otel/src/test/java/com/onesignal/otel/OneSignalOpenTelemetryTest.kt +++ b/OneSignalSDK/onesignal/otel/src/test/java/com/onesignal/otel/OneSignalOpenTelemetryTest.kt @@ -56,13 +56,13 @@ class OneSignalOpenTelemetryTest : FunSpec({ } test("remote telemetry should have logExporter") { - val remoteTelemetry = OtelFactory.createRemoteTelemetry(mockPlatformProvider) + val remoteTelemetry = OtelFactory.createRemoteTelemetry(mockPlatformProvider) as IOtelSdkRemoteTelemetry remoteTelemetry.logExporter shouldNotBe null } test("remote telemetry getLogger should return LogRecordBuilder") { - val remoteTelemetry = OtelFactory.createRemoteTelemetry(mockPlatformProvider) + val remoteTelemetry = OtelFactory.createRemoteTelemetry(mockPlatformProvider) as IOtelSdkRemoteTelemetry runBlocking { val logger = remoteTelemetry.getLogger() @@ -71,7 +71,7 @@ class OneSignalOpenTelemetryTest : FunSpec({ } test("remote telemetry forceFlush should not throw") { - val remoteTelemetry = OtelFactory.createRemoteTelemetry(mockPlatformProvider) + val remoteTelemetry = OtelFactory.createRemoteTelemetry(mockPlatformProvider) as IOtelSdkRemoteTelemetry runBlocking { // Should not throw @@ -111,7 +111,7 @@ class OneSignalOpenTelemetryTest : FunSpec({ every { mockPlatformProvider.crashStoragePath } returns tempDir try { - val crashTelemetry = OtelFactory.createCrashLocalTelemetry(mockPlatformProvider) + val crashTelemetry = OtelFactory.createCrashLocalTelemetry(mockPlatformProvider) as IOtelSdkCrashTelemetry runBlocking { val logger = crashTelemetry.getLogger() @@ -140,7 +140,7 @@ class OneSignalOpenTelemetryTest : FunSpec({ // ===== SDK Caching Tests ===== test("remote telemetry should cache SDK instance") { - val remoteTelemetry = OtelFactory.createRemoteTelemetry(mockPlatformProvider) + val remoteTelemetry = OtelFactory.createRemoteTelemetry(mockPlatformProvider) as IOtelSdkRemoteTelemetry runBlocking { val logger1 = remoteTelemetry.getLogger() diff --git a/OneSignalSDK/onesignal/otel/src/test/java/com/onesignal/otel/OtelFactoryTest.kt b/OneSignalSDK/onesignal/otel/src/test/java/com/onesignal/otel/OtelFactoryTest.kt index 56f2ce5cc4..2e758596ba 100644 --- a/OneSignalSDK/onesignal/otel/src/test/java/com/onesignal/otel/OtelFactoryTest.kt +++ b/OneSignalSDK/onesignal/otel/src/test/java/com/onesignal/otel/OtelFactoryTest.kt @@ -105,6 +105,7 @@ class OtelFactoryTest : FunSpec({ val telemetry = OtelFactory.createRemoteTelemetry(mockPlatformProvider) // Then + telemetry.shouldBeInstanceOf() telemetry.logExporter shouldNotBe null } diff --git a/OneSignalSDK/onesignal/otel/src/test/java/com/onesignal/otel/OtelLoggingHelperTest.kt b/OneSignalSDK/onesignal/otel/src/test/java/com/onesignal/otel/OtelLoggingHelperTest.kt index 16b195754e..4f35cffaf8 100644 --- a/OneSignalSDK/onesignal/otel/src/test/java/com/onesignal/otel/OtelLoggingHelperTest.kt +++ b/OneSignalSDK/onesignal/otel/src/test/java/com/onesignal/otel/OtelLoggingHelperTest.kt @@ -13,7 +13,7 @@ import io.opentelemetry.api.logs.Severity import kotlinx.coroutines.runBlocking class OtelLoggingHelperTest : FunSpec({ - val mockTelemetry = mockk(relaxed = true) + val mockTelemetry = mockk(relaxed = true) val mockLogRecordBuilder = mockk(relaxed = true) beforeEach { diff --git a/OneSignalSDK/onesignal/otel/src/test/java/com/onesignal/otel/crash/OtelCrashReporterTest.kt b/OneSignalSDK/onesignal/otel/src/test/java/com/onesignal/otel/crash/OtelCrashReporterTest.kt index e94591d833..255258432d 100644 --- a/OneSignalSDK/onesignal/otel/src/test/java/com/onesignal/otel/crash/OtelCrashReporterTest.kt +++ b/OneSignalSDK/onesignal/otel/src/test/java/com/onesignal/otel/crash/OtelCrashReporterTest.kt @@ -2,7 +2,7 @@ package com.onesignal.otel.crash import com.onesignal.otel.IOtelCrashReporter import com.onesignal.otel.IOtelLogger -import com.onesignal.otel.IOtelOpenTelemetryCrash +import com.onesignal.otel.IOtelSdkCrashTelemetry import io.kotest.assertions.throwables.shouldThrow import io.kotest.core.spec.style.FunSpec import io.kotest.matchers.shouldBe @@ -22,7 +22,7 @@ import io.opentelemetry.sdk.common.CompletableResultCode import kotlinx.coroutines.runBlocking class OtelCrashReporterTest : FunSpec({ - val mockOpenTelemetry = mockk(relaxed = true) + val mockOpenTelemetry = mockk(relaxed = true) val mockLogger = mockk(relaxed = true) val mockLogRecordBuilder = mockk(relaxed = true) val mockCompletableResult = mockk(relaxed = true) diff --git a/OneSignalSDK/onesignal/otel/src/test/java/com/onesignal/otel/crash/OtelCrashUploaderTest.kt b/OneSignalSDK/onesignal/otel/src/test/java/com/onesignal/otel/crash/OtelCrashUploaderTest.kt index 3f8ffb0a54..7253456d12 100644 --- a/OneSignalSDK/onesignal/otel/src/test/java/com/onesignal/otel/crash/OtelCrashUploaderTest.kt +++ b/OneSignalSDK/onesignal/otel/src/test/java/com/onesignal/otel/crash/OtelCrashUploaderTest.kt @@ -1,8 +1,8 @@ package com.onesignal.otel.crash import com.onesignal.otel.IOtelLogger -import com.onesignal.otel.IOtelOpenTelemetryRemote import com.onesignal.otel.IOtelPlatformProvider +import com.onesignal.otel.IOtelSdkRemoteTelemetry import io.kotest.matchers.shouldBe import io.kotest.matchers.shouldNotBe import io.mockk.every @@ -18,7 +18,7 @@ import org.junit.Test import java.io.File class OtelCrashUploaderTest { - private lateinit var mockRemoteTelemetry: IOtelOpenTelemetryRemote + private lateinit var mockRemoteTelemetry: IOtelSdkRemoteTelemetry private lateinit var mockPlatformProvider: IOtelPlatformProvider private lateinit var mockLogger: IOtelLogger private lateinit var mockExporter: LogRecordExporter diff --git a/OneSignalSDK/settings.gradle b/OneSignalSDK/settings.gradle index d385e2a3bd..b35d36de33 100644 --- a/OneSignalSDK/settings.gradle +++ b/OneSignalSDK/settings.gradle @@ -1,3 +1,11 @@ +pluginManagement { + repositories { + google() + mavenCentral() + gradlePluginPortal() + } +} + // Allows running the :app project above with the local source for development of the SDK. // This means we can keep the example app as-is so it stays as a real world example. gradle.rootProject { diff --git a/examples/demo/app/build.gradle.kts b/examples/demo/app/build.gradle.kts index 96e09074e4..5525145d72 100644 --- a/examples/demo/app/build.gradle.kts +++ b/examples/demo/app/build.gradle.kts @@ -175,3 +175,28 @@ dependencies { "huaweiImplementation"("com.huawei.hms:push:6.3.0.304") "huaweiImplementation"("com.huawei.hms:location:4.0.0.300") } + +// Simulates a host app (or another SDK such as Embrace) that pulls OpenTelemetry 1.64 +// while OneSignal still compiles against 1.55 + disk-buffering 1.51.0-alpha. +// Used by CI to prove the relocated OTel copy in com.onesignal:otel does not clash +// (SDK-5006 / https://github.com/OneSignal/OneSignal-Android-SDK/issues/2714). +if (project.hasProperty("simulateOtelClash")) { + configurations.configureEach { + resolutionStrategy { + force("io.opentelemetry:opentelemetry-api:1.64.0") + force("io.opentelemetry:opentelemetry-sdk:1.64.0") + force("io.opentelemetry:opentelemetry-sdk-common:1.64.0") + force("io.opentelemetry:opentelemetry-sdk-logs:1.64.0") + force("io.opentelemetry:opentelemetry-sdk-metrics:1.64.0") + force("io.opentelemetry:opentelemetry-sdk-trace:1.64.0") + force("io.opentelemetry:opentelemetry-exporter-otlp:1.64.0") + force("io.opentelemetry:opentelemetry-exporter-otlp-common:1.64.0") + } + } + dependencies { + implementation(platform("io.opentelemetry:opentelemetry-bom:1.64.0")) + implementation("io.opentelemetry:opentelemetry-api") + implementation("io.opentelemetry:opentelemetry-sdk") + implementation("io.opentelemetry:opentelemetry-sdk-logs") + } +} diff --git a/examples/demo/app/proguard-rules.pro b/examples/demo/app/proguard-rules.pro index 574f565e4b..27a2b96516 100644 --- a/examples/demo/app/proguard-rules.pro +++ b/examples/demo/app/proguard-rules.pro @@ -22,4 +22,7 @@ # No app-level -dontwarn for OneSignal OTel here: when com.onesignal:core pulls in com.onesignal:otel # (implementation dependency), AGP merges otel's consumer-rules.pro for R8 (SDK-4207 / #2596). -# Older SDK lines without otel never put those optional classes on the classpath, so duplicates are unnecessary. +# OpenTelemetry Java packages are relocated into com.onesignal.shaded.opentelemetry inside the otel +# AAR (SDK-5006 / #2714), so a host OpenTelemetry BOM (e.g. Embrace 1.64) cannot clash with +# OneSignal at R8 time. Older SDK lines without otel never put those optional classes on the +# classpath, so duplicates are unnecessary.