Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -63,6 +64,7 @@ class OtelAnrDetectorTest : FunSpec({
backgroundThresholdMs = 10_000L,
isAppInForeground = inForeground,
platform = platform,
crashReporter = mockk<IOtelCrashReporter>(relaxed = true),
)

test("OtelAnrDetector implements IOtelAnrDetector") {
Expand Down
223 changes: 217 additions & 6 deletions OneSignalSDK/onesignal/otel/build.gradle
Original file line number Diff line number Diff line change
@@ -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'
Expand All @@ -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
Expand Down Expand Up @@ -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")
Expand All @@ -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'
27 changes: 15 additions & 12 deletions OneSignalSDK/onesignal/otel/consumer-rules.pro
Original file line number Diff line number Diff line change
@@ -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.**
Original file line number Diff line number Diff line change
@@ -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.).
Expand All @@ -40,6 +24,4 @@ interface IOtelOpenTelemetryCrash : IOtelOpenTelemetry
/**
* Interface for remote OpenTelemetry (network export).
*/
interface IOtelOpenTelemetryRemote : IOtelOpenTelemetry {
val logExporter: LogRecordExporter
}
interface IOtelOpenTelemetryRemote : IOtelOpenTelemetry
Original file line number Diff line number Diff line change
@@ -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
}
Loading