From 7d68d8345cafa550fee8d07ca615a6552e7d0f99 Mon Sep 17 00:00:00 2001 From: Josh Kasten Date: Sun, 1 Feb 2026 16:47:38 -0500 Subject: [PATCH 01/15] fix: inverse LogLevel logic for omitting This was backwards causing ERROR to send everything --- .../main/java/com/onesignal/internal/OneSignalCrashLogInit.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/internal/OneSignalCrashLogInit.kt b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/internal/OneSignalCrashLogInit.kt index d69b25fd2..28653f3b9 100644 --- a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/internal/OneSignalCrashLogInit.kt +++ b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/internal/OneSignalCrashLogInit.kt @@ -100,7 +100,7 @@ internal class OneSignalCrashLogInit( val shouldSendLogLevel: (LogLevel) -> Boolean = { level -> when { remoteLogLevel == LogLevel.NONE -> false // Don't send anything - else -> level >= remoteLogLevel // Send at configured level and above + else -> level <= remoteLogLevel } } From a9b833e9ec77f690b2cb32fd15ec86db5207e752 Mon Sep 17 00:00:00 2001 From: Josh Kasten Date: Sun, 1 Feb 2026 16:51:19 -0500 Subject: [PATCH 02/15] chore: clean up initializeOtelLogging Simplified logic, cleaned up logging statements, and removed code comments that added no value. --- .../internal/OneSignalCrashLogInit.kt | 43 ++++++++----------- 1 file changed, 17 insertions(+), 26 deletions(-) diff --git a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/internal/OneSignalCrashLogInit.kt b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/internal/OneSignalCrashLogInit.kt index 28653f3b9..d668904ed 100644 --- a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/internal/OneSignalCrashLogInit.kt +++ b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/internal/OneSignalCrashLogInit.kt @@ -70,49 +70,40 @@ internal class OneSignalCrashLogInit( fun initializeOtelLogging() { // Initialize Otel logging asynchronously to avoid blocking initialization // Remote logging is not critical for crashes, so it's safe to do this in the background - // Uses OtelIdResolver internally which reads directly from SharedPreferences - // No service dependencies required - fully decoupled from service architecture + // To remove the variable of refactors of internal dependencies breaking this + // initialization, OtelIdResolver reads directly from SharedPreferences, to fully decouple + // from our service architecture. suspendifyOnIO { try { - // Reuses the same platform provider instance created for crash handler // Get the remote log level as string (defaults to "ERROR" if null, "NONE" if explicitly set) val remoteLogLevelStr = platformProvider.remoteLogLevel - // Check if remote logging is enabled (not NONE) if (remoteLogLevelStr != null && remoteLogLevelStr != "NONE") { - // Store in local variable for smart cast - val logLevelStr = remoteLogLevelStr - Logging.info("OneSignal: Remote logging enabled at level $logLevelStr, initializing Otel logging integration...") - val remoteTelemetry: IOtelOpenTelemetryRemote = OtelFactory.createRemoteTelemetry(platformProvider) + Logging.debug("OneSignal: Remote logging enabled at level $remoteLogLevelStr, initializing Otel logging integration...") + val remoteTelemetry = OtelFactory.createRemoteTelemetry(platformProvider) - // Parse the log level string to LogLevel enum for comparison - @Suppress("TooGenericExceptionCaught", "SwallowedException") - val remoteLogLevel: LogLevel = try { - LogLevel.valueOf(logLevelStr) - } catch (e: Exception) { - LogLevel.ERROR // Default to ERROR on parse error - } + val remoteLogLevel = + try { + LogLevel.valueOf(remoteLogLevelStr) + } catch (_: Exception) { + LogLevel.ERROR + } - // Create a function that checks if a log level should be sent remotely - // - If remoteLogLevel is null: default to ERROR (send ERROR and above) - // - If remoteLogLevel is NONE: don't send anything (shouldn't reach here, but handle it) - // - Otherwise: send logs at that level and above val shouldSendLogLevel: (LogLevel) -> Boolean = { level -> when { - remoteLogLevel == LogLevel.NONE -> false // Don't send anything + remoteLogLevel == LogLevel.NONE -> false else -> level <= remoteLogLevel } } - // Inject Otel telemetry into Logging class Logging.setOtelTelemetry(remoteTelemetry, shouldSendLogLevel) - Logging.info("OneSignal: ✅ Otel logging integration initialized - logs at level $logLevelStr and above will be sent to remote server") + Logging.debug("OneSignal: ✅ Otel logging integration initialized - logs at level $remoteLogLevelStr and above will be sent to remote server") } else { - Logging.debug("OneSignal: Remote logging disabled (level: $remoteLogLevelStr), skipping Otel logging integration") + Logging.debug("OneSignal: ❌ Remote logging disabled (level: $remoteLogLevelStr), skipping Otel logging integration") } - } catch (e: Exception) { - // If Otel logging initialization fails, log it but don't crash - Logging.warn("OneSignal: Failed to initialize Otel logging: ${e.message}", e) + } catch (t: Throwable) { + // Never crash the app if Otel fails to initialize. + Logging.warn("OneSignal: Failed to initialize Otel logging: ${t.message}", t) } } } From f40064663603ceb57bf2fffe6851141e18f8cfe6 Mon Sep 17 00:00:00 2001 From: Josh Kasten Date: Sun, 1 Feb 2026 16:57:50 -0500 Subject: [PATCH 03/15] chore: clean up parseLogLevel Removed "old" format support, we never shipped that. Removed int format, we never shipped nor plan to change it. Removed "logLevel" field fallback, we will always just use "log_level" --- .../backend/impl/ParamsBackendService.kt | 30 ++++--------------- 1 file changed, 6 insertions(+), 24 deletions(-) diff --git a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/core/internal/backend/impl/ParamsBackendService.kt b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/core/internal/backend/impl/ParamsBackendService.kt index b2d68a411..3ee022ffc 100644 --- a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/core/internal/backend/impl/ParamsBackendService.kt +++ b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/core/internal/backend/impl/ParamsBackendService.kt @@ -136,36 +136,18 @@ internal class ParamsBackendService( } /** - * Parse LogLevel from JSON. Supports both string (enum name) and int (ordinal) formats. + * Parse LogLevel from JSON. Supports string (enum name) */ - @Suppress("ReturnCount", "TooGenericExceptionCaught", "SwallowedException") private fun parseLogLevel(json: JSONObject): LogLevel? { - // Try string format first (e.g., "ERROR", "WARN", "NONE") - val logLevelString = json.safeString("log_level") ?: json.safeString("logLevel") - if (logLevelString != null) { + val logLevel = json.safeString("log_level") + if (logLevel != null) { try { - return LogLevel.valueOf(logLevelString.uppercase()) - } catch (e: IllegalArgumentException) { - Logging.warn("Invalid log level string: $logLevelString") + return LogLevel.valueOf(logLevel.uppercase()) + } catch (_: IllegalArgumentException) { + Logging.warn("Invalid log_level string: $logLevel") } } - // Try int format (ordinal: 0=NONE, 1=FATAL, 2=ERROR, etc.) - val logLevelInt = json.safeInt("log_level") ?: json.safeInt("logLevel") - if (logLevelInt != null) { - try { - return LogLevel.fromInt(logLevelInt) - } catch (e: Exception) { - Logging.warn("Invalid log level int: $logLevelInt") - } - } - - // Backward compatibility: support old "enable" boolean field - val enable = json.safeBool("enable") - if (enable != null) { - return if (enable) LogLevel.ERROR else LogLevel.NONE - } - return null } } From 9a8954d9e5343ab0e82bada5653375d732bd373b Mon Sep 17 00:00:00 2001 From: Josh Kasten Date: Sun, 1 Feb 2026 17:06:51 -0500 Subject: [PATCH 04/15] fix: lower severity of main thread log In the context of remote logging this is "info", to the app developer this is more of a warning, however this is something we will develop later. --- .../core/src/main/java/com/onesignal/internal/OneSignalImp.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/internal/OneSignalImp.kt b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/internal/OneSignalImp.kt index 4de2a4a73..39fc28326 100644 --- a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/internal/OneSignalImp.kt +++ b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/internal/OneSignalImp.kt @@ -443,7 +443,7 @@ internal class OneSignalImp( private fun blockingGet(getter: () -> T): T { try { if (AndroidUtils.isRunningOnMainThread()) { - Logging.warn("This is called on main thread. This is not recommended.") + Logging.info("This is called on main thread. This is not recommended.") } } catch (e: RuntimeException) { // In test environments, AndroidUtils.isRunningOnMainThread() may fail From 8f9c30545f86fa74d91f445450765bd4de1b0925 Mon Sep 17 00:00:00 2001 From: Josh Kasten Date: Sun, 1 Feb 2026 21:00:55 -0500 Subject: [PATCH 05/15] fix: processUptime Looks like this was broke in a large refactor in commit 76460ee --- .../internal/logging/otel/android/OtelPlatformProvider.kt | 5 +++-- .../onesignal/debug/internal/crash/CrashReportUploadTest.kt | 2 +- .../onesignal/debug/internal/crash/OtelIntegrationTest.kt | 3 ++- .../logging/otel/android/OtelPlatformProviderTest.kt | 6 ++++-- .../main/java/com/onesignal/otel/IOtelPlatformProvider.kt | 2 +- .../src/test/java/com/onesignal/otel/OtelFactoryTest.kt | 2 +- .../com/onesignal/otel/attributes/OtelFieldsPerEventTest.kt | 6 +++--- 7 files changed, 15 insertions(+), 11 deletions(-) diff --git a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/debug/internal/logging/otel/android/OtelPlatformProvider.kt b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/debug/internal/logging/otel/android/OtelPlatformProvider.kt index 84b48ecfc..a1528846e 100644 --- a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/debug/internal/logging/otel/android/OtelPlatformProvider.kt +++ b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/debug/internal/logging/otel/android/OtelPlatformProvider.kt @@ -3,6 +3,7 @@ package com.onesignal.debug.internal.logging.otel.android import android.app.ActivityManager import android.content.Context import android.os.Build +import android.os.SystemClock import com.onesignal.common.OneSignalUtils import com.onesignal.common.OneSignalWrapper import com.onesignal.debug.internal.logging.Logging @@ -100,8 +101,8 @@ internal class OtelPlatformProvider( } // https://opentelemetry.io/docs/specs/semconv/system/process-metrics/#metric-processuptime - override val processUptime: Double - get() = android.os.SystemClock.uptimeMillis() / 1_000.0 // Use SystemClock directly + override val processUptime: Long + get() = SystemClock.uptimeMillis() - android.os.Process.getStartUptimeMillis() // https://opentelemetry.io/docs/specs/semconv/general/attributes/#general-thread-attributes override val currentThreadName: String diff --git a/OneSignalSDK/onesignal/core/src/test/java/com/onesignal/debug/internal/crash/CrashReportUploadTest.kt b/OneSignalSDK/onesignal/core/src/test/java/com/onesignal/debug/internal/crash/CrashReportUploadTest.kt index 9327dd9f7..6893992a5 100644 --- a/OneSignalSDK/onesignal/core/src/test/java/com/onesignal/debug/internal/crash/CrashReportUploadTest.kt +++ b/OneSignalSDK/onesignal/core/src/test/java/com/onesignal/debug/internal/crash/CrashReportUploadTest.kt @@ -277,7 +277,7 @@ class CrashReportUploadTest : FunSpec({ println(" OneSignal ID: ${platformProvider.onesignalId}") println(" Push Subscription ID: ${platformProvider.pushSubscriptionId}") println(" App State: ${platformProvider.appState}") - println(" Process Uptime: ${platformProvider.processUptime}s") + println(" Process Uptime: ${platformProvider.processUptime}ms") println(" Thread Name: ${platformProvider.currentThreadName}") println(" Remote Log Level: ${platformProvider.remoteLogLevel}") diff --git a/OneSignalSDK/onesignal/core/src/test/java/com/onesignal/debug/internal/crash/OtelIntegrationTest.kt b/OneSignalSDK/onesignal/core/src/test/java/com/onesignal/debug/internal/crash/OtelIntegrationTest.kt index bcce46424..42c153f6c 100644 --- a/OneSignalSDK/onesignal/core/src/test/java/com/onesignal/debug/internal/crash/OtelIntegrationTest.kt +++ b/OneSignalSDK/onesignal/core/src/test/java/com/onesignal/debug/internal/crash/OtelIntegrationTest.kt @@ -15,6 +15,7 @@ import com.onesignal.otel.IOtelPlatformProvider import com.onesignal.otel.OtelFactory import com.onesignal.user.internal.backend.IdentityConstants import io.kotest.core.spec.style.FunSpec +import io.kotest.matchers.comparables.shouldBeGreaterThan import io.kotest.matchers.shouldBe import io.kotest.matchers.shouldNotBe import io.kotest.matchers.types.shouldBeInstanceOf @@ -111,7 +112,7 @@ class OtelIntegrationTest : FunSpec({ provider.onesignalId shouldBe "test-onesignal-id" provider.pushSubscriptionId shouldBe "test-subscription-id" provider.appState shouldBeOneOf listOf("foreground", "background", "unknown") - (provider.processUptime > 0.0) shouldBe true + provider.processUptime shouldBeGreaterThan 0 provider.currentThreadName shouldBe Thread.currentThread().name } diff --git a/OneSignalSDK/onesignal/core/src/test/java/com/onesignal/debug/internal/logging/otel/android/OtelPlatformProviderTest.kt b/OneSignalSDK/onesignal/core/src/test/java/com/onesignal/debug/internal/logging/otel/android/OtelPlatformProviderTest.kt index 1aea4be88..d7aab7d11 100644 --- a/OneSignalSDK/onesignal/core/src/test/java/com/onesignal/debug/internal/logging/otel/android/OtelPlatformProviderTest.kt +++ b/OneSignalSDK/onesignal/core/src/test/java/com/onesignal/debug/internal/logging/otel/android/OtelPlatformProviderTest.kt @@ -14,6 +14,8 @@ import com.onesignal.debug.LogLevel import com.onesignal.debug.internal.logging.Logging import com.onesignal.user.internal.backend.IdentityConstants import io.kotest.core.spec.style.FunSpec +import io.kotest.matchers.comparables.shouldBeGreaterThan +import io.kotest.matchers.comparables.shouldBeLessThan import io.kotest.matchers.shouldBe import io.kotest.matchers.shouldNotBe import io.kotest.matchers.string.shouldContain @@ -416,8 +418,8 @@ class OtelPlatformProviderTest : FunSpec({ val result = provider.processUptime // Then - (result > 0.0) shouldBe true - (result < 1000000.0) shouldBe true // Reasonable upper bound + result shouldBeGreaterThan 0 + result shouldBeLessThan 1000000 // Reasonable upper bound } // ===== currentThreadName Tests ===== diff --git a/OneSignalSDK/onesignal/otel/src/main/java/com/onesignal/otel/IOtelPlatformProvider.kt b/OneSignalSDK/onesignal/otel/src/main/java/com/onesignal/otel/IOtelPlatformProvider.kt index 83dfdb335..8f49f6877 100644 --- a/OneSignalSDK/onesignal/otel/src/main/java/com/onesignal/otel/IOtelPlatformProvider.kt +++ b/OneSignalSDK/onesignal/otel/src/main/java/com/onesignal/otel/IOtelPlatformProvider.kt @@ -30,7 +30,7 @@ interface IOtelPlatformProvider { val onesignalId: String? val pushSubscriptionId: String? val appState: String // "foreground" or "background" - val processUptime: Double // in seconds + val processUptime: Long // in ms val currentThreadName: String // Crash-specific configuration 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 cf29a0f21..8fb1da716 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 @@ -29,7 +29,7 @@ class OtelFactoryTest : FunSpec({ every { mockPlatformProvider.onesignalId } returns null every { mockPlatformProvider.pushSubscriptionId } returns null every { mockPlatformProvider.appState } returns "foreground" - every { mockPlatformProvider.processUptime } returns 100.0 + every { mockPlatformProvider.processUptime } returns 100 every { mockPlatformProvider.currentThreadName } returns "main" every { mockPlatformProvider.crashStoragePath } returns "/test/path" every { mockPlatformProvider.minFileAgeForReadMillis } returns 5000L diff --git a/OneSignalSDK/onesignal/otel/src/test/java/com/onesignal/otel/attributes/OtelFieldsPerEventTest.kt b/OneSignalSDK/onesignal/otel/src/test/java/com/onesignal/otel/attributes/OtelFieldsPerEventTest.kt index 5d64c5ab2..46e26bf91 100644 --- a/OneSignalSDK/onesignal/otel/src/test/java/com/onesignal/otel/attributes/OtelFieldsPerEventTest.kt +++ b/OneSignalSDK/onesignal/otel/src/test/java/com/onesignal/otel/attributes/OtelFieldsPerEventTest.kt @@ -18,7 +18,7 @@ class OtelFieldsPerEventTest : FunSpec({ every { mockPlatformProvider.onesignalId } returns "test-onesignal-id" every { mockPlatformProvider.pushSubscriptionId } returns "test-subscription-id" every { mockPlatformProvider.appState } returns "foreground" - every { mockPlatformProvider.processUptime } returns 100.5 + every { mockPlatformProvider.processUptime } returns 100 every { mockPlatformProvider.currentThreadName } returns "main-thread" val attributes = fields.getAttributes() @@ -38,7 +38,7 @@ class OtelFieldsPerEventTest : FunSpec({ every { mockPlatformProvider.onesignalId } returns null every { mockPlatformProvider.pushSubscriptionId } returns null every { mockPlatformProvider.appState } returns "background" - every { mockPlatformProvider.processUptime } returns 50.0 + every { mockPlatformProvider.processUptime } returns 50 every { mockPlatformProvider.currentThreadName } returns "worker-thread" val attributes = fields.getAttributes() @@ -56,7 +56,7 @@ class OtelFieldsPerEventTest : FunSpec({ every { mockPlatformProvider.onesignalId } returns null every { mockPlatformProvider.pushSubscriptionId } returns null every { mockPlatformProvider.appState } returns "foreground" - every { mockPlatformProvider.processUptime } returns 100.0 + every { mockPlatformProvider.processUptime } returns 100 every { mockPlatformProvider.currentThreadName } returns "main" val attributes1 = fields.getAttributes() From 05ca3c4af7b0ef4e13a2d8d610aafab026db131e Mon Sep 17 00:00:00 2001 From: Josh Kasten Date: Sun, 1 Feb 2026 21:57:19 -0500 Subject: [PATCH 06/15] fix: account for otel missing class Classes Otel depends on will be missing on Android 7 and older devices, or if the app is heavily minified. Added specific handling for this as well as an outer catch on Throwable so we at least allow any existing crash handlers to run. --- .../onesignal/otel/crash/OtelCrashHandler.kt | 42 +++++++++++++------ 1 file changed, 30 insertions(+), 12 deletions(-) diff --git a/OneSignalSDK/onesignal/otel/src/main/java/com/onesignal/otel/crash/OtelCrashHandler.kt b/OneSignalSDK/onesignal/otel/src/main/java/com/onesignal/otel/crash/OtelCrashHandler.kt index ebe744e04..766d75496 100644 --- a/OneSignalSDK/onesignal/otel/src/main/java/com/onesignal/otel/crash/OtelCrashHandler.kt +++ b/OneSignalSDK/onesignal/otel/src/main/java/com/onesignal/otel/crash/OtelCrashHandler.kt @@ -15,7 +15,8 @@ import kotlinx.coroutines.runBlocking internal class OtelCrashHandler( private val crashReporter: IOtelCrashReporter, private val logger: IOtelLogger, -) : Thread.UncaughtExceptionHandler, com.onesignal.otel.IOtelCrashHandler { +) : Thread.UncaughtExceptionHandler, + com.onesignal.otel.IOtelCrashHandler { private var existingHandler: Thread.UncaughtExceptionHandler? = null private val seenThrowables: MutableList = mutableListOf() private var initialized = false @@ -33,9 +34,7 @@ internal class OtelCrashHandler( } override fun uncaughtException(thread: Thread, throwable: Throwable) { - // Ensure we never attempt to process the same throwable instance - // more than once. This would only happen if there was another crash - // handler and was faulty in a specific way. + // Prevents infinite loops if another handler calls us again synchronized(seenThrowables) { if (seenThrowables.contains(throwable)) { logger.warn("OtelCrashHandler: Ignoring duplicate throwable instance") @@ -44,12 +43,26 @@ internal class OtelCrashHandler( seenThrowables.add(throwable) } + try { + internalUncaughtException(thread, throwable) + } catch (t: Throwable) { + // If there is a bug with our crash handling we at least want to + // ensure the existingHandler is not skipped. + logger.error("Error thrown when saving crash report: ${t.message}") + } + + logger.debug("OtelCrashHandler: Delegating to existing crash handler") + existingHandler?.uncaughtException(thread, throwable) + } + + private fun internalUncaughtException(thread: Thread, throwable: Throwable) { logger.info("OtelCrashHandler: Uncaught exception detected - ${throwable.javaClass.simpleName}: ${throwable.message}") // Check if this is an ANR exception (though standalone ANR detector already handles ANRs) // This would only catch ANRs if they're thrown as exceptions, which is rare - val isAnr = throwable.javaClass.simpleName.contains("ApplicationNotResponding", ignoreCase = true) || - throwable.message?.contains("Application Not Responding", ignoreCase = true) == true + val isAnr = + throwable.javaClass.simpleName.contains("ApplicationNotResponding", ignoreCase = true) || + throwable.message?.contains("Application Not Responding", ignoreCase = true) == true // NOTE: Future improvements: // - Catch anything we may throw and print only to logcat @@ -61,7 +74,6 @@ internal class OtelCrashHandler( // thrown as exceptions (unlikely), and we still check if OneSignal is at fault. if (!isAnr && !isOneSignalAtFault(throwable)) { logger.debug("OtelCrashHandler: Crash is not OneSignal-related, delegating to existing handler") - existingHandler?.uncaughtException(thread, throwable) return } @@ -70,7 +82,10 @@ internal class OtelCrashHandler( } logger.info("OtelCrashHandler: OneSignal-related crash detected, saving crash report...") + saveCrash(thread, throwable) + } + private fun saveCrash(thread: Thread, throwable: Throwable) { /** * NOTE: The order and running sequentially is important as: * The existingHandler.uncaughtException can immediately terminate the @@ -92,16 +107,19 @@ internal class OtelCrashHandler( logger.info("OtelCrashHandler: Crash report saved successfully") } catch (e: RuntimeException) { // If crash reporting fails, at least try to log it - logger.error("OtelCrashHandler: Failed to save crash report: ${e.message} - ${e.javaClass.simpleName}") + logger.error("OtelCrashHandler: Runtime error, could not save crash report: ${e.message} - ${e.javaClass.simpleName}") } catch (e: java.io.IOException) { // Handle IO errors specifically - logger.error("OtelCrashHandler: IO error saving crash report: ${e.message}") + logger.error("OtelCrashHandler: IO error, could not save crash report: ${e.message}") } catch (e: IllegalStateException) { // Handle illegal state errors - logger.error("OtelCrashHandler: Illegal state error saving crash report: ${e.message}") + logger.error("OtelCrashHandler: Illegal state error, could not save crash report: ${e.message}") + } catch (e: NoClassDefFoundError) { + logger.error( + "OtelCrashHandler: No Class found error, this happens on Android 7 or older, " + + "or if parts of otel code was omitted from the app, could not save crash report: ${e.message}" + ) } - logger.info("OtelCrashHandler: Delegating to existing crash handler") - existingHandler?.uncaughtException(thread, throwable) } } From 1bf6a259170ba376d530fa11d02ded11f0fc3291 Mon Sep 17 00:00:00 2001 From: Josh Kasten Date: Sun, 1 Feb 2026 22:29:25 -0500 Subject: [PATCH 07/15] fix: crash on otel usage on Android 7 and older --- .../debug/internal/crash/OneSignalCrashUploaderWrapper.kt | 4 ++++ .../main/java/com/onesignal/debug/internal/logging/Logging.kt | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/debug/internal/crash/OneSignalCrashUploaderWrapper.kt b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/debug/internal/crash/OneSignalCrashUploaderWrapper.kt index e9d620d09..2da16418f 100644 --- a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/debug/internal/crash/OneSignalCrashUploaderWrapper.kt +++ b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/debug/internal/crash/OneSignalCrashUploaderWrapper.kt @@ -1,5 +1,6 @@ package com.onesignal.debug.internal.crash +import android.os.Build import com.onesignal.core.internal.application.IApplicationService import com.onesignal.core.internal.startup.IStartableService import com.onesignal.debug.internal.logging.otel.android.AndroidOtelLogger @@ -44,6 +45,9 @@ internal class OneSignalCrashUploaderWrapper( } override fun start() { + // Otel library requires Android Oreo (8) or newer + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) return + runBlocking { uploader.start() } diff --git a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/debug/internal/logging/Logging.kt b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/debug/internal/logging/Logging.kt index d305fe64e..64e0020e3 100644 --- a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/debug/internal/logging/Logging.kt +++ b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/debug/internal/logging/Logging.kt @@ -1,6 +1,7 @@ package com.onesignal.debug.internal.logging import android.app.AlertDialog +import android.os.Build import com.onesignal.common.threading.suspendifyOnMain import com.onesignal.core.internal.application.IApplicationService import com.onesignal.debug.ILogListener @@ -217,6 +218,9 @@ object Logging { // Check if this log level should be sent remotely if (!shouldSendLogLevel(level)) return + // Otel library requires Android Oreo (8) or newer + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) return + // Log asynchronously (non-blocking) otelLoggingScope.launch { try { From 3c604c5d969138b8971fe274658f4c276a6eaa12 Mon Sep 17 00:00:00 2001 From: Josh Kasten Date: Sun, 1 Feb 2026 22:32:18 -0500 Subject: [PATCH 08/15] fix: catch Throwable on otel access We don't want logging to be the root cause of an app crash. --- .../debug/internal/crash/OneSignalCrashUploaderWrapper.kt | 7 ++++++- .../java/com/onesignal/debug/internal/logging/Logging.kt | 4 ++-- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/debug/internal/crash/OneSignalCrashUploaderWrapper.kt b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/debug/internal/crash/OneSignalCrashUploaderWrapper.kt index 2da16418f..df26c5fdd 100644 --- a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/debug/internal/crash/OneSignalCrashUploaderWrapper.kt +++ b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/debug/internal/crash/OneSignalCrashUploaderWrapper.kt @@ -3,6 +3,7 @@ package com.onesignal.debug.internal.crash import android.os.Build import com.onesignal.core.internal.application.IApplicationService import com.onesignal.core.internal.startup.IStartableService +import com.onesignal.debug.internal.logging.Logging import com.onesignal.debug.internal.logging.otel.android.AndroidOtelLogger import com.onesignal.debug.internal.logging.otel.android.createAndroidOtelPlatformProvider import com.onesignal.otel.OtelFactory @@ -49,7 +50,11 @@ internal class OneSignalCrashUploaderWrapper( if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) return runBlocking { - uploader.start() + try { + uploader.start() + } catch (t: Throwable) { + Logging.error("Error attempting to upload crash log", t) + } } } } diff --git a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/debug/internal/logging/Logging.kt b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/debug/internal/logging/Logging.kt index 64e0020e3..d31d8d7ec 100644 --- a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/debug/internal/logging/Logging.kt +++ b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/debug/internal/logging/Logging.kt @@ -232,9 +232,9 @@ object Logging { exceptionMessage = throwable?.message, exceptionStacktrace = throwable?.stackTraceToString(), ) - } catch (e: Exception) { + } catch (e: Throwable) { // Don't log Otel errors to Otel (would cause infinite loop) - // Just log to logcat silently + // Just print to logcat android.util.Log.e(TAG, "Failed to log to Otel: ${e.message}", e) } } From e41a735b7b4920f9d751bab6a33774a66988b39b Mon Sep 17 00:00:00 2001 From: Josh Kasten Date: Sun, 1 Feb 2026 22:34:03 -0500 Subject: [PATCH 09/15] fix: lower severity of IAM paused log --- .../onesignal/inAppMessages/internal/InAppMessagesManager.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/OneSignalSDK/onesignal/in-app-messages/src/main/java/com/onesignal/inAppMessages/internal/InAppMessagesManager.kt b/OneSignalSDK/onesignal/in-app-messages/src/main/java/com/onesignal/inAppMessages/internal/InAppMessagesManager.kt index 2b68fe345..35020d128 100644 --- a/OneSignalSDK/onesignal/in-app-messages/src/main/java/com/onesignal/inAppMessages/internal/InAppMessagesManager.kt +++ b/OneSignalSDK/onesignal/in-app-messages/src/main/java/com/onesignal/inAppMessages/internal/InAppMessagesManager.kt @@ -419,7 +419,7 @@ internal class InAppMessagesManager( Logging.debug("InAppMessagesManager.attemptToShowInAppMessage: $messageDisplayQueue") // If there are IAMs in the queue and nothing showing, show first in the queue if (paused) { - Logging.warn( + Logging.info( "InAppMessagesManager.attemptToShowInAppMessage: In app messaging is currently paused, in app messages will not be shown!", ) } else if (messageDisplayQueue.isEmpty()) { From 2656318b54db03f0cde0f4b73d0d20ec5433a167 Mon Sep 17 00:00:00 2001 From: Josh Kasten Date: Sun, 8 Feb 2026 20:56:01 -0500 Subject: [PATCH 10/15] fix: address Build.VERSION usage and tests --- .../crash/OneSignalCrashUploaderWrapper.kt | 3 +- .../debug/internal/logging/Logging.kt | 3 +- .../debug/internal/logging/LoggingOtelTest.kt | 361 ++++++++++-------- 3 files changed, 210 insertions(+), 157 deletions(-) diff --git a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/debug/internal/crash/OneSignalCrashUploaderWrapper.kt b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/debug/internal/crash/OneSignalCrashUploaderWrapper.kt index df26c5fdd..03090afd8 100644 --- a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/debug/internal/crash/OneSignalCrashUploaderWrapper.kt +++ b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/debug/internal/crash/OneSignalCrashUploaderWrapper.kt @@ -1,6 +1,7 @@ package com.onesignal.debug.internal.crash import android.os.Build +import com.onesignal.common.AndroidUtils import com.onesignal.core.internal.application.IApplicationService import com.onesignal.core.internal.startup.IStartableService import com.onesignal.debug.internal.logging.Logging @@ -47,7 +48,7 @@ internal class OneSignalCrashUploaderWrapper( override fun start() { // Otel library requires Android Oreo (8) or newer - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) return + if (AndroidUtils.androidSDKInt < Build.VERSION_CODES.O) return runBlocking { try { diff --git a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/debug/internal/logging/Logging.kt b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/debug/internal/logging/Logging.kt index d31d8d7ec..e51b46e56 100644 --- a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/debug/internal/logging/Logging.kt +++ b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/debug/internal/logging/Logging.kt @@ -21,6 +21,7 @@ object Logging { private const val TAG = "OneSignal" var applicationService: IApplicationService? = null + var androidVersion: Int = Build.VERSION.SDK_INT private val logListeners = CopyOnWriteArraySet() @@ -219,7 +220,7 @@ object Logging { if (!shouldSendLogLevel(level)) return // Otel library requires Android Oreo (8) or newer - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) return + if (androidVersion < Build.VERSION_CODES.O) return // Log asynchronously (non-blocking) otelLoggingScope.launch { diff --git a/OneSignalSDK/onesignal/core/src/test/java/com/onesignal/debug/internal/logging/LoggingOtelTest.kt b/OneSignalSDK/onesignal/core/src/test/java/com/onesignal/debug/internal/logging/LoggingOtelTest.kt index 6bde1defb..f0f40b2fa 100644 --- a/OneSignalSDK/onesignal/core/src/test/java/com/onesignal/debug/internal/logging/LoggingOtelTest.kt +++ b/OneSignalSDK/onesignal/core/src/test/java/com/onesignal/debug/internal/logging/LoggingOtelTest.kt @@ -4,229 +4,280 @@ import android.os.Build import br.com.colman.kotest.android.extensions.robolectric.RobolectricTest import com.onesignal.debug.LogLevel import com.onesignal.otel.IOtelOpenTelemetryRemote +import com.onesignal.otel.OtelLoggingHelper import io.kotest.core.spec.style.FunSpec +import io.mockk.coVerify import io.mockk.mockk +import io.mockk.mockkObject +import io.mockk.unmockkObject import kotlinx.coroutines.delay import kotlinx.coroutines.runBlocking import org.robolectric.annotation.Config @RobolectricTest @Config(sdk = [Build.VERSION_CODES.O]) -class LoggingOtelTest : FunSpec({ - val mockTelemetry = mockk(relaxed = true) +class LoggingOtelTest : + FunSpec({ + val mockTelemetry = mockk(relaxed = true) - beforeEach { - // Reset Logging state - Logging.setOtelTelemetry(null, { false }) + beforeEach { + // Reset Logging state + Logging.setOtelTelemetry(null, { false }) - // Setup default mock behavior - relaxed mock automatically returns mocks for suspend functions - // The return type (LogRecordBuilder) is handled by the relaxed mock, but we can't verify it - // directly due to type visibility. We'll test behavior instead. - } + // Setup default mock behavior - relaxed mock automatically returns mocks for suspend functions + // The return type (LogRecordBuilder) is handled by the relaxed mock, but we can't verify it + // directly due to type visibility. We'll test behavior instead. + } + + test("setOtelTelemetry should store telemetry and enabled check function") { + // Given + val shouldSend = { _: LogLevel -> true } - test("setOtelTelemetry should store telemetry and enabled check function") { - // Given - val shouldSend = { _: LogLevel -> true } + // When + Logging.setOtelTelemetry(mockTelemetry, shouldSend) - // When - Logging.setOtelTelemetry(mockTelemetry, shouldSend) + // Then - verify it's set (we'll test it works by logging) + Logging.info("test") - // Then - verify it's set (we'll test it works by logging) - Logging.info("test") + // Wait for async logging + runBlocking { + delay(100) + } - // Wait for async logging - runBlocking { - delay(100) + // Then - verify it doesn't crash (integration test) + // Note: We can't verify exact calls due to OpenTelemetry type visibility } - // Then - verify it doesn't crash (integration test) - // Note: We can't verify exact calls due to OpenTelemetry type visibility - } + test("logToOtel should work when remote logging is enabled") { + // Given + Logging.setOtelTelemetry(mockTelemetry, { _: LogLevel -> true }) - test("logToOtel should work when remote logging is enabled") { - // Given - Logging.setOtelTelemetry(mockTelemetry, { _: LogLevel -> true }) + // When + Logging.info("test message") - // When - Logging.info("test message") + // Wait for async logging + runBlocking { + delay(100) + } - // Wait for async logging - runBlocking { - delay(100) + // Then - should not crash (integration test) + // The actual Otel call is verified in otel module tests } - // Then - should not crash (integration test) - // The actual Otel call is verified in otel module tests - } + test("logToOtel should NOT crash when remote logging is disabled") { + // Given + Logging.setOtelTelemetry(mockTelemetry, { _: LogLevel -> false }) - test("logToOtel should NOT crash when remote logging is disabled") { - // Given - Logging.setOtelTelemetry(mockTelemetry, { _: LogLevel -> false }) + // When + Logging.info("test message") - // When - Logging.info("test message") + // Wait for async logging + runBlocking { + delay(100) + } - // Wait for async logging - runBlocking { - delay(100) + // Then - should not crash } - // Then - should not crash - } + test("logToOtel should NOT crash when telemetry is null") { + // Given + Logging.setOtelTelemetry(null, { _: LogLevel -> true }) - test("logToOtel should NOT crash when telemetry is null") { - // Given - Logging.setOtelTelemetry(null, { _: LogLevel -> true }) + // When + Logging.info("test message") - // When - Logging.info("test message") + // Wait for async logging + runBlocking { + delay(100) + } - // Wait for async logging - runBlocking { - delay(100) + // Then - should not crash } - // Then - should not crash - } + test("logToOtel should handle all log levels without crashing") { + // Given + Logging.setOtelTelemetry(mockTelemetry, { _: LogLevel -> true }) - test("logToOtel should handle all log levels without crashing") { - // Given - Logging.setOtelTelemetry(mockTelemetry, { _: LogLevel -> true }) + // When + Logging.verbose("verbose message") + Logging.debug("debug message") + Logging.info("info message") + Logging.warn("warn message") + Logging.error("error message") + Logging.fatal("fatal message") - // When - Logging.verbose("verbose message") - Logging.debug("debug message") - Logging.info("info message") - Logging.warn("warn message") - Logging.error("error message") - Logging.fatal("fatal message") + // Wait for async logging + runBlocking { + delay(200) + } - // Wait for async logging - runBlocking { - delay(200) + // Then - should not crash for any level } - // Then - should not crash for any level - } + test("logToOtel should NOT log NONE level") { + // Given + Logging.setOtelTelemetry(mockTelemetry, { _: LogLevel -> true }) - test("logToOtel should NOT log NONE level") { - // Given - Logging.setOtelTelemetry(mockTelemetry, { _: LogLevel -> true }) + // When + Logging.log(LogLevel.NONE, "none message") - // When - Logging.log(LogLevel.NONE, "none message") + // Wait for async logging + runBlocking { + delay(100) + } - // Wait for async logging - runBlocking { - delay(100) + // Then - should not crash, NONE level is skipped } - // Then - should not crash, NONE level is skipped - } + test("logToOtel should handle exceptions in logs") { + // Given + Logging.setOtelTelemetry(mockTelemetry, { _: LogLevel -> true }) + val exception = RuntimeException("test exception") - test("logToOtel should handle exceptions in logs") { - // Given - Logging.setOtelTelemetry(mockTelemetry, { _: LogLevel -> true }) - val exception = RuntimeException("test exception") + // When + Logging.error("error with exception", exception) - // When - Logging.error("error with exception", exception) + // Wait for async logging + runBlocking { + delay(100) + } - // Wait for async logging - runBlocking { - delay(100) + // Then - should not crash, exception details are included } - // Then - should not crash, exception details are included - } + test("logToOtel should handle null exception message") { + // Given + Logging.setOtelTelemetry(mockTelemetry, { _: LogLevel -> true }) + val exception = RuntimeException() - test("logToOtel should handle null exception message") { - // Given - Logging.setOtelTelemetry(mockTelemetry, { _: LogLevel -> true }) - val exception = RuntimeException() + // When + Logging.error("error with null exception message", exception) - // When - Logging.error("error with null exception message", exception) + // Wait for async logging + runBlocking { + delay(100) + } - // Wait for async logging - runBlocking { - delay(100) + // Then - should not crash } - // Then - should not crash - } + test("logToOtel should handle Otel errors gracefully") { + // Given + Logging.setOtelTelemetry(mockTelemetry, { _: LogLevel -> true }) + // Note: We can't mock getLogger() to throw due to OpenTelemetry type visibility, + // but the real implementation in Logging.logToOtel() handles errors gracefully - test("logToOtel should handle Otel errors gracefully") { - // Given - Logging.setOtelTelemetry(mockTelemetry, { _: LogLevel -> true }) - // Note: We can't mock getLogger() to throw due to OpenTelemetry type visibility, - // but the real implementation in Logging.logToOtel() handles errors gracefully + // When + Logging.info("test message") - // When - Logging.info("test message") + // Wait for async logging + runBlocking { + delay(100) + } - // Wait for async logging - runBlocking { - delay(100) + // Then - should not crash, error handling is tested in integration tests } - // Then - should not crash, error handling is tested in integration tests - } + test("logToOtel should use dynamic remote logging check") { + // Given + var isEnabled = false + Logging.setOtelTelemetry(mockTelemetry, { _: LogLevel -> isEnabled }) - test("logToOtel should use dynamic remote logging check") { - // Given - var isEnabled = false - Logging.setOtelTelemetry(mockTelemetry, { _: LogLevel -> isEnabled }) + // When - initially disabled + Logging.info("message 1") + runBlocking { delay(50) } - // When - initially disabled - Logging.info("message 1") - runBlocking { delay(50) } + // When - enable remote logging + isEnabled = true + Logging.info("message 2") + runBlocking { delay(50) } - // When - enable remote logging - isEnabled = true - Logging.info("message 2") - runBlocking { delay(50) } + // When - disable again + isEnabled = false + Logging.info("message 3") + runBlocking { delay(50) } - // When - disable again - isEnabled = false - Logging.info("message 3") - runBlocking { delay(50) } + // Then - should not crash, dynamic check works + } - // Then - should not crash, dynamic check works - } + test("logToOtel should handle multiple rapid log calls") { + // Given + Logging.setOtelTelemetry(mockTelemetry, { _: LogLevel -> true }) - test("logToOtel should handle multiple rapid log calls") { - // Given - Logging.setOtelTelemetry(mockTelemetry, { _: LogLevel -> true }) + // When - rapid fire logging + repeat(10) { + Logging.info("message $it") + } - // When - rapid fire logging - repeat(10) { - Logging.info("message $it") - } + // Wait for async logging + runBlocking { + delay(200) + } - // Wait for async logging - runBlocking { - delay(200) + // Then - should not crash } - // Then - should not crash - } + test("logToOtel should work with different message formats") { + // Given + Logging.setOtelTelemetry(mockTelemetry, { _: LogLevel -> true }) - test("logToOtel should work with different message formats") { - // Given - Logging.setOtelTelemetry(mockTelemetry, { _: LogLevel -> true }) + // When + Logging.info("simple message") + Logging.info("message with numbers: 12345") + Logging.info("message with special chars: !@#$%") + Logging.info("message with unicode: 测试 🚀") - // When - Logging.info("simple message") - Logging.info("message with numbers: 12345") - Logging.info("message with special chars: !@#$%") - Logging.info("message with unicode: 测试 🚀") + // Wait for async logging + runBlocking { + delay(200) + } - // Wait for async logging - runBlocking { - delay(200) + // Then - should not crash } - // Then - should not crash - } -}) + test("logToOtel should work on Android 8 and newer") { + // Given + mockkObject(OtelLoggingHelper) + Logging.setOtelTelemetry(mockTelemetry, { _: LogLevel -> true }) + Logging.androidVersion = Build.VERSION_CODES.O + + // When + Logging.fatal("simple message") + + coVerify(exactly = 1) { + OtelLoggingHelper.logToOtel( + any(), + any(), + any(), + any(), + any(), + any(), + ) + } + + unmockkObject(OtelLoggingHelper) + } + + test("logToOtel should skip Android 7 and older") { + // Given + mockkObject(OtelLoggingHelper) + Logging.setOtelTelemetry(mockTelemetry, { _: LogLevel -> true }) + Logging.androidVersion = Build.VERSION_CODES.N + + // When + Logging.fatal("simple message") + + coVerify(exactly = 0) { + OtelLoggingHelper.logToOtel( + any(), + any(), + any(), + any(), + any(), + any(), + ) + } + + unmockkObject(OtelLoggingHelper) + } + }) From b377734d6c93e0a342e0433f4e369e542d0757e8 Mon Sep 17 00:00:00 2001 From: Josh Kasten Date: Sun, 8 Feb 2026 20:56:40 -0500 Subject: [PATCH 11/15] fix: default logging to NONE on parse error --- .../main/java/com/onesignal/internal/OneSignalCrashLogInit.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/internal/OneSignalCrashLogInit.kt b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/internal/OneSignalCrashLogInit.kt index d668904ed..1739a9ccb 100644 --- a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/internal/OneSignalCrashLogInit.kt +++ b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/internal/OneSignalCrashLogInit.kt @@ -86,7 +86,7 @@ internal class OneSignalCrashLogInit( try { LogLevel.valueOf(remoteLogLevelStr) } catch (_: Exception) { - LogLevel.ERROR + LogLevel.NONE } val shouldSendLogLevel: (LogLevel) -> Boolean = { level -> From f8e7b4aabe78610e9d7912dcbab871b95d98f2c3 Mon Sep 17 00:00:00 2001 From: Josh Kasten Date: Sun, 8 Feb 2026 20:59:45 -0500 Subject: [PATCH 12/15] fix: address log entries --- .../core/src/main/java/com/onesignal/internal/OneSignalImp.kt | 2 +- .../src/main/java/com/onesignal/otel/crash/OtelCrashHandler.kt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/internal/OneSignalImp.kt b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/internal/OneSignalImp.kt index 39fc28326..041800a37 100644 --- a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/internal/OneSignalImp.kt +++ b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/internal/OneSignalImp.kt @@ -443,7 +443,7 @@ internal class OneSignalImp( private fun blockingGet(getter: () -> T): T { try { if (AndroidUtils.isRunningOnMainThread()) { - Logging.info("This is called on main thread. This is not recommended.") + Logging.debug("This is called on main thread. This is not recommended.") } } catch (e: RuntimeException) { // In test environments, AndroidUtils.isRunningOnMainThread() may fail diff --git a/OneSignalSDK/onesignal/otel/src/main/java/com/onesignal/otel/crash/OtelCrashHandler.kt b/OneSignalSDK/onesignal/otel/src/main/java/com/onesignal/otel/crash/OtelCrashHandler.kt index 766d75496..4aad37a3b 100644 --- a/OneSignalSDK/onesignal/otel/src/main/java/com/onesignal/otel/crash/OtelCrashHandler.kt +++ b/OneSignalSDK/onesignal/otel/src/main/java/com/onesignal/otel/crash/OtelCrashHandler.kt @@ -116,7 +116,7 @@ internal class OtelCrashHandler( logger.error("OtelCrashHandler: Illegal state error, could not save crash report: ${e.message}") } catch (e: NoClassDefFoundError) { logger.error( - "OtelCrashHandler: No Class found error, this happens on Android 7 or older, " + + "OtelCrashHandler: NoClassDefFoundError, this happens on Android 7 or older, " + "or if parts of otel code was omitted from the app, could not save crash report: ${e.message}" ) } From dd14a1504db1db73eac940ba4941405fbf173f84 Mon Sep 17 00:00:00 2001 From: Josh Kasten Date: Sun, 8 Feb 2026 21:05:48 -0500 Subject: [PATCH 13/15] fix: return NONE on paring error --- .../core/internal/backend/impl/ParamsBackendService.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/core/internal/backend/impl/ParamsBackendService.kt b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/core/internal/backend/impl/ParamsBackendService.kt index 3ee022ffc..f5dd0bac1 100644 --- a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/core/internal/backend/impl/ParamsBackendService.kt +++ b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/core/internal/backend/impl/ParamsBackendService.kt @@ -138,7 +138,7 @@ internal class ParamsBackendService( /** * Parse LogLevel from JSON. Supports string (enum name) */ - private fun parseLogLevel(json: JSONObject): LogLevel? { + private fun parseLogLevel(json: JSONObject): LogLevel { val logLevel = json.safeString("log_level") if (logLevel != null) { try { @@ -148,6 +148,6 @@ internal class ParamsBackendService( } } - return null + return LogLevel.NONE } } From 1b121ef3e536cce4d42cd8e7b49d891ea0fefe91 Mon Sep 17 00:00:00 2001 From: AR Abdul Azeez Date: Tue, 24 Feb 2026 15:37:19 -0500 Subject: [PATCH 14/15] fix: centralize SDK version check into OtelSdkSupport Addresses review feedback to consolidate the scattered Build.VERSION.SDK_INT < Build.VERSION_CODES.O checks into a single OtelSdkSupport utility object. - OtelSdkSupport.isSupported replaces inline version checks in Logging, OneSignalCrashUploaderWrapper, and OneSignalCrashHandlerFactory - Factory now uses require() instead of returning a NoOpCrashHandler - OtelSdkSupport.isSupported is writable for unit tests - Removed Logging.androidVersion field (replaced by OtelSdkSupport) - Updated all affected tests Co-authored-by: Cursor --- .../crash/OneSignalCrashHandlerFactory.kt | 27 ++++--------- .../crash/OneSignalCrashUploaderWrapper.kt | 5 +-- .../debug/internal/crash/OtelSdkSupport.kt | 27 +++++++++++++ .../debug/internal/logging/Logging.kt | 6 +-- .../internal/OneSignalCrashLogInit.kt | 1 - .../crash/OneSignalCrashHandlerFactoryTest.kt | 37 ++++++++++-------- .../internal/crash/OtelSdkSupportTest.kt | 38 +++++++++++++++++++ .../debug/internal/logging/LoggingOtelTest.kt | 11 +++--- 8 files changed, 102 insertions(+), 50 deletions(-) create mode 100644 OneSignalSDK/onesignal/core/src/main/java/com/onesignal/debug/internal/crash/OtelSdkSupport.kt create mode 100644 OneSignalSDK/onesignal/core/src/test/java/com/onesignal/debug/internal/crash/OtelSdkSupportTest.kt diff --git a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/debug/internal/crash/OneSignalCrashHandlerFactory.kt b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/debug/internal/crash/OneSignalCrashHandlerFactory.kt index 162a4f7e0..1ed9c0cec 100644 --- a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/debug/internal/crash/OneSignalCrashHandlerFactory.kt +++ b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/debug/internal/crash/OneSignalCrashHandlerFactory.kt @@ -1,7 +1,6 @@ package com.onesignal.debug.internal.crash import android.content.Context -import android.os.Build import com.onesignal.debug.internal.logging.Logging import com.onesignal.debug.internal.logging.otel.android.createAndroidOtelPlatformProvider import com.onesignal.otel.IOtelCrashHandler @@ -9,43 +8,31 @@ import com.onesignal.otel.IOtelLogger import com.onesignal.otel.OtelFactory /** - * Factory for creating crash handlers with SDK version checks. - * For SDK < 26, returns a no-op implementation. - * For SDK >= 26, returns the Otel-based crash handler. + * Factory for creating Otel-based crash handlers. + * Callers must verify [OtelSdkSupport.isSupported] before calling [createCrashHandler]. * * Uses minimal dependencies - only Context and logger. * Platform provider uses OtelIdResolver internally which reads from SharedPreferences. */ internal object OneSignalCrashHandlerFactory { /** - * Creates a crash handler appropriate for the current SDK version. + * Creates the Otel crash handler. * This should be called as early as possible, before any other initialization. * * @param context Android context for creating platform provider * @param logger Logger instance (can be shared with other components) + * @throws IllegalArgumentException if called on an unsupported SDK */ fun createCrashHandler( context: Context, logger: IOtelLogger, ): IOtelCrashHandler { - // Otel requires SDK 26+, use no-op for older versions - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) { - Logging.info("OneSignal: Creating no-op crash handler (SDK ${Build.VERSION.SDK_INT} < 26)") - return NoOpCrashHandler() + require(OtelSdkSupport.isSupported) { + "createCrashHandler called on unsupported SDK (< ${OtelSdkSupport.MIN_SDK_VERSION})" } - Logging.info("OneSignal: Creating Otel crash handler (SDK ${Build.VERSION.SDK_INT} >= 26)") - // Create platform provider - uses OtelIdResolver internally + Logging.info("OneSignal: Creating Otel crash handler (SDK >= ${OtelSdkSupport.MIN_SDK_VERSION})") val platformProvider = createAndroidOtelPlatformProvider(context) return OtelFactory.createCrashHandler(platformProvider, logger) } } - -/** - * No-op crash handler for SDK < 26. - */ -private class NoOpCrashHandler : IOtelCrashHandler { - override fun initialize() { - Logging.info("OneSignal: No-op crash handler initialized (SDK < 26, Otel not supported)") - } -} diff --git a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/debug/internal/crash/OneSignalCrashUploaderWrapper.kt b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/debug/internal/crash/OneSignalCrashUploaderWrapper.kt index 03090afd8..d918a59e2 100644 --- a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/debug/internal/crash/OneSignalCrashUploaderWrapper.kt +++ b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/debug/internal/crash/OneSignalCrashUploaderWrapper.kt @@ -1,7 +1,5 @@ package com.onesignal.debug.internal.crash -import android.os.Build -import com.onesignal.common.AndroidUtils import com.onesignal.core.internal.application.IApplicationService import com.onesignal.core.internal.startup.IStartableService import com.onesignal.debug.internal.logging.Logging @@ -47,8 +45,7 @@ internal class OneSignalCrashUploaderWrapper( } override fun start() { - // Otel library requires Android Oreo (8) or newer - if (AndroidUtils.androidSDKInt < Build.VERSION_CODES.O) return + if (!OtelSdkSupport.isSupported) return runBlocking { try { diff --git a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/debug/internal/crash/OtelSdkSupport.kt b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/debug/internal/crash/OtelSdkSupport.kt new file mode 100644 index 000000000..47fc0034d --- /dev/null +++ b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/debug/internal/crash/OtelSdkSupport.kt @@ -0,0 +1,27 @@ +package com.onesignal.debug.internal.crash + +import android.os.Build + +/** + * Centralizes the SDK version requirement for Otel-based features + * (crash reporting, ANR detection, remote log shipping). + * + * [isSupported] is writable internally so that unit tests can override + * the device-level gate without Robolectric @Config gymnastics. + */ +internal object OtelSdkSupport { + /** Otel libraries require Android O (API 26) or above. */ + const val MIN_SDK_VERSION = Build.VERSION_CODES.O // 26 + + /** + * Whether the current device meets the minimum SDK requirement. + * Production code should treat this as read-only; tests may flip it via [reset]/direct set. + */ + var isSupported: Boolean = Build.VERSION.SDK_INT >= MIN_SDK_VERSION + internal set + + /** Restores the runtime-detected value — call in test teardown. */ + fun reset() { + isSupported = Build.VERSION.SDK_INT >= MIN_SDK_VERSION + } +} diff --git a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/debug/internal/logging/Logging.kt b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/debug/internal/logging/Logging.kt index e51b46e56..585aa8789 100644 --- a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/debug/internal/logging/Logging.kt +++ b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/debug/internal/logging/Logging.kt @@ -1,12 +1,12 @@ package com.onesignal.debug.internal.logging import android.app.AlertDialog -import android.os.Build import com.onesignal.common.threading.suspendifyOnMain import com.onesignal.core.internal.application.IApplicationService import com.onesignal.debug.ILogListener import com.onesignal.debug.LogLevel import com.onesignal.debug.OneSignalLogEvent +import com.onesignal.debug.internal.crash.OtelSdkSupport import com.onesignal.otel.IOtelOpenTelemetryRemote import com.onesignal.otel.OtelLoggingHelper import kotlinx.coroutines.CoroutineScope @@ -21,7 +21,6 @@ object Logging { private const val TAG = "OneSignal" var applicationService: IApplicationService? = null - var androidVersion: Int = Build.VERSION.SDK_INT private val logListeners = CopyOnWriteArraySet() @@ -219,8 +218,7 @@ object Logging { // Check if this log level should be sent remotely if (!shouldSendLogLevel(level)) return - // Otel library requires Android Oreo (8) or newer - if (androidVersion < Build.VERSION_CODES.O) return + if (!OtelSdkSupport.isSupported) return // Log asynchronously (non-blocking) otelLoggingScope.launch { diff --git a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/internal/OneSignalCrashLogInit.kt b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/internal/OneSignalCrashLogInit.kt index 1739a9ccb..c913ab619 100644 --- a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/internal/OneSignalCrashLogInit.kt +++ b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/internal/OneSignalCrashLogInit.kt @@ -9,7 +9,6 @@ import com.onesignal.debug.internal.logging.otel.android.AndroidOtelLogger import com.onesignal.debug.internal.logging.otel.android.OtelPlatformProvider import com.onesignal.debug.internal.logging.otel.android.createAndroidOtelPlatformProvider import com.onesignal.otel.IOtelCrashHandler -import com.onesignal.otel.IOtelOpenTelemetryRemote import com.onesignal.otel.OtelFactory import com.onesignal.otel.crash.IOtelAnrDetector diff --git a/OneSignalSDK/onesignal/core/src/test/java/com/onesignal/debug/internal/crash/OneSignalCrashHandlerFactoryTest.kt b/OneSignalSDK/onesignal/core/src/test/java/com/onesignal/debug/internal/crash/OneSignalCrashHandlerFactoryTest.kt index 3b71b4d05..9c0140a89 100644 --- a/OneSignalSDK/onesignal/core/src/test/java/com/onesignal/debug/internal/crash/OneSignalCrashHandlerFactoryTest.kt +++ b/OneSignalSDK/onesignal/core/src/test/java/com/onesignal/debug/internal/crash/OneSignalCrashHandlerFactoryTest.kt @@ -1,15 +1,19 @@ package com.onesignal.debug.internal.crash import android.content.Context +import android.os.Build import androidx.test.core.app.ApplicationProvider import br.com.colman.kotest.android.extensions.robolectric.RobolectricTest import com.onesignal.debug.internal.logging.otel.android.AndroidOtelLogger import com.onesignal.otel.IOtelCrashHandler +import io.kotest.assertions.throwables.shouldThrow import io.kotest.core.spec.style.FunSpec import io.kotest.matchers.shouldNotBe import io.kotest.matchers.types.shouldBeInstanceOf +import org.robolectric.annotation.Config @RobolectricTest +@Config(sdk = [Build.VERSION_CODES.O]) class OneSignalCrashHandlerFactoryTest : FunSpec({ var appContext: Context? = null var logger: AndroidOtelLogger? = null @@ -21,37 +25,38 @@ class OneSignalCrashHandlerFactoryTest : FunSpec({ } } - test("createCrashHandler should return IOtelCrashHandler") { + afterEach { + OtelSdkSupport.reset() + } + + test("createCrashHandler should return IOtelCrashHandler when supported") { + OtelSdkSupport.isSupported = true val handler = OneSignalCrashHandlerFactory.createCrashHandler( appContext!!, - logger!! + logger!!, ) handler.shouldBeInstanceOf() } test("createCrashHandler should create Otel handler for SDK 26+") { - // Note: SDK version check is handled at runtime by the factory - // This test verifies the handler can be created and initialized + OtelSdkSupport.isSupported = true val handler = OneSignalCrashHandlerFactory.createCrashHandler( appContext!!, - logger!! + logger!!, ) handler shouldNotBe null - // Should be able to initialize handler.initialize() } - test("createCrashHandler should return no-op handler for SDK < 26") { - // Note: SDK version check is handled at runtime by the factory - // This test verifies the handler can be created and initialized - val handler = OneSignalCrashHandlerFactory.createCrashHandler( - appContext!!, - logger!! - ) - - handler shouldNotBe null - handler.initialize() // Should not crash + test("createCrashHandler should throw when SDK is unsupported") { + OtelSdkSupport.isSupported = false + shouldThrow { + OneSignalCrashHandlerFactory.createCrashHandler( + appContext!!, + logger!!, + ) + } } }) diff --git a/OneSignalSDK/onesignal/core/src/test/java/com/onesignal/debug/internal/crash/OtelSdkSupportTest.kt b/OneSignalSDK/onesignal/core/src/test/java/com/onesignal/debug/internal/crash/OtelSdkSupportTest.kt new file mode 100644 index 000000000..f7660108e --- /dev/null +++ b/OneSignalSDK/onesignal/core/src/test/java/com/onesignal/debug/internal/crash/OtelSdkSupportTest.kt @@ -0,0 +1,38 @@ +package com.onesignal.debug.internal.crash + +import android.os.Build +import br.com.colman.kotest.android.extensions.robolectric.RobolectricTest +import io.kotest.core.spec.style.FunSpec +import io.kotest.matchers.shouldBe +import org.robolectric.annotation.Config + +@RobolectricTest +@Config(sdk = [Build.VERSION_CODES.O]) +class OtelSdkSupportTest : FunSpec({ + + afterEach { + OtelSdkSupport.reset() + } + + test("isSupported is true on SDK >= 26") { + OtelSdkSupport.reset() + OtelSdkSupport.isSupported shouldBe true + } + + test("isSupported can be overridden to false for testing") { + OtelSdkSupport.isSupported = false + OtelSdkSupport.isSupported shouldBe false + } + + test("reset restores runtime-detected value") { + OtelSdkSupport.isSupported = false + OtelSdkSupport.isSupported shouldBe false + + OtelSdkSupport.reset() + OtelSdkSupport.isSupported shouldBe true + } + + test("MIN_SDK_VERSION is 26") { + OtelSdkSupport.MIN_SDK_VERSION shouldBe 26 + } +}) diff --git a/OneSignalSDK/onesignal/core/src/test/java/com/onesignal/debug/internal/logging/LoggingOtelTest.kt b/OneSignalSDK/onesignal/core/src/test/java/com/onesignal/debug/internal/logging/LoggingOtelTest.kt index f0f40b2fa..c1558a864 100644 --- a/OneSignalSDK/onesignal/core/src/test/java/com/onesignal/debug/internal/logging/LoggingOtelTest.kt +++ b/OneSignalSDK/onesignal/core/src/test/java/com/onesignal/debug/internal/logging/LoggingOtelTest.kt @@ -3,6 +3,7 @@ package com.onesignal.debug.internal.logging import android.os.Build import br.com.colman.kotest.android.extensions.robolectric.RobolectricTest import com.onesignal.debug.LogLevel +import com.onesignal.debug.internal.crash.OtelSdkSupport import com.onesignal.otel.IOtelOpenTelemetryRemote import com.onesignal.otel.OtelLoggingHelper import io.kotest.core.spec.style.FunSpec @@ -23,10 +24,10 @@ class LoggingOtelTest : beforeEach { // Reset Logging state Logging.setOtelTelemetry(null, { false }) + } - // Setup default mock behavior - relaxed mock automatically returns mocks for suspend functions - // The return type (LogRecordBuilder) is handled by the relaxed mock, but we can't verify it - // directly due to type visibility. We'll test behavior instead. + afterEach { + OtelSdkSupport.reset() } test("setOtelTelemetry should store telemetry and enabled check function") { @@ -239,7 +240,7 @@ class LoggingOtelTest : // Given mockkObject(OtelLoggingHelper) Logging.setOtelTelemetry(mockTelemetry, { _: LogLevel -> true }) - Logging.androidVersion = Build.VERSION_CODES.O + OtelSdkSupport.isSupported = true // When Logging.fatal("simple message") @@ -262,7 +263,7 @@ class LoggingOtelTest : // Given mockkObject(OtelLoggingHelper) Logging.setOtelTelemetry(mockTelemetry, { _: LogLevel -> true }) - Logging.androidVersion = Build.VERSION_CODES.N + OtelSdkSupport.isSupported = false // When Logging.fatal("simple message") From 134a91c700186fd0998091e0472639b18e7d1bc9 Mon Sep 17 00:00:00 2001 From: AR Abdul Azeez Date: Tue, 24 Feb 2026 15:40:52 -0500 Subject: [PATCH 15/15] chore: remove CrashReportUploadTest Deleted in base branch by cleanup PR #2541. Co-authored-by: Cursor --- .../internal/crash/CrashReportUploadTest.kt | 336 ------------------ 1 file changed, 336 deletions(-) delete mode 100644 OneSignalSDK/onesignal/core/src/test/java/com/onesignal/debug/internal/crash/CrashReportUploadTest.kt diff --git a/OneSignalSDK/onesignal/core/src/test/java/com/onesignal/debug/internal/crash/CrashReportUploadTest.kt b/OneSignalSDK/onesignal/core/src/test/java/com/onesignal/debug/internal/crash/CrashReportUploadTest.kt deleted file mode 100644 index 6893992a5..000000000 --- a/OneSignalSDK/onesignal/core/src/test/java/com/onesignal/debug/internal/crash/CrashReportUploadTest.kt +++ /dev/null @@ -1,336 +0,0 @@ -package com.onesignal.debug.internal.crash - -import android.content.Context -import android.content.SharedPreferences -import android.os.Build -import androidx.test.core.app.ApplicationProvider -import br.com.colman.kotest.android.extensions.robolectric.RobolectricTest -import com.onesignal.core.internal.config.ConfigModel -import com.onesignal.core.internal.preferences.PreferenceOneSignalKeys -import com.onesignal.core.internal.preferences.PreferenceStores -import com.onesignal.debug.LogLevel -import com.onesignal.debug.internal.logging.Logging -import com.onesignal.debug.internal.logging.otel.android.createAndroidOtelPlatformProvider -import com.onesignal.otel.IOtelOpenTelemetryRemote -import com.onesignal.otel.OtelFactory -import com.onesignal.otel.OtelLoggingHelper -import com.onesignal.user.internal.backend.IdentityConstants -import io.kotest.core.spec.style.FunSpec -import io.kotest.matchers.shouldBe -import io.kotest.matchers.shouldNotBe -import io.kotest.matchers.types.shouldBeInstanceOf -import kotlinx.coroutines.delay -import kotlinx.coroutines.runBlocking -import org.json.JSONArray -import org.json.JSONObject -import org.robolectric.annotation.Config -import java.util.UUID -import com.onesignal.core.internal.config.CONFIG_NAME_SPACE as configNameSpace -import com.onesignal.user.internal.identity.IDENTITY_NAME_SPACE as identityNameSpace - -/** - * Integration test that uploads a sample crash report to the OneSignal API. - * - * This test sends a real HTTP request to the API endpoint configured in OtelConfigRemoteOneSignal. - * - * To use this test: - * 1. Set a valid app ID in the test (replace "YOUR_APP_ID_HERE") - * 2. Ensure the API endpoint is accessible (check OtelConfigRemoteOneSignal.BASE_URL) - * 3. Run the test and verify the crash report appears in your backend - * - * Note: This test requires network access and will make a real HTTP request. - * - * Android Studio Note: If tests fail in Android Studio but work on command line: - * - File → Invalidate Caches → Invalidate and Restart - * - File → Sync Project with Gradle Files - * - Ensure you're running as "Unit Test" (not "Instrumented Test") - * - Try running from command line: ./gradlew :onesignal:core:testDebugUnitTest --tests "CrashReportUploadTest" - */ -@RobolectricTest -@Config(sdk = [Build.VERSION_CODES.O]) -class CrashReportUploadTest : FunSpec({ - var appContext: Context? = null - var sharedPreferences: SharedPreferences? = null - - // TODO: Replace with your actual app ID for testing - val testAppId = "YOUR_APP_ID_HERE" - - beforeAny { - if (appContext == null) { - appContext = ApplicationProvider.getApplicationContext() - sharedPreferences = appContext!!.getSharedPreferences(PreferenceStores.ONESIGNAL, Context.MODE_PRIVATE) - } - } - - beforeSpec { - // Enable debug logging to see what's being sent - Logging.logLevel = LogLevel.DEBUG - Logging.info("🔍 Debug logging enabled for CrashReportUploadTest") - println("🔍 Debug logging enabled") - } - - beforeEach { - // Ensure sharedPreferences is initialized - if (sharedPreferences == null) { - appContext = ApplicationProvider.getApplicationContext() - sharedPreferences = appContext!!.getSharedPreferences(PreferenceStores.ONESIGNAL, Context.MODE_PRIVATE) - } - // Clear and set up SharedPreferences with test data - sharedPreferences!!.edit().clear().commit() - - // Set up ConfigModelStore data - val configModel = JSONObject().apply { - put(ConfigModel::appId.name, testAppId) - put(ConfigModel::pushSubscriptionId.name, "test-subscription-id-${UUID.randomUUID()}") - val remoteLoggingParams = JSONObject().apply { - put("logLevel", "ERROR") - } - put(ConfigModel::remoteLoggingParams.name, remoteLoggingParams) - } - val configArray = JSONArray().apply { - put(configModel) - } - - // Set up IdentityModelStore data - val identityModel = JSONObject().apply { - put(IdentityConstants.ONESIGNAL_ID, "test-onesignal-id-${UUID.randomUUID()}") - } - val identityArray = JSONArray().apply { - put(identityModel) - } - - sharedPreferences.edit() - .putString(PreferenceOneSignalKeys.MODEL_STORE_PREFIX + configNameSpace, configArray.toString()) - .putString(PreferenceOneSignalKeys.MODEL_STORE_PREFIX + identityNameSpace, identityArray.toString()) - .putString(PreferenceOneSignalKeys.PREFS_OS_INSTALL_ID, UUID.randomUUID().toString()) - .commit() - } - - afterEach { - sharedPreferences!!.edit().clear().commit() - } - - test("should upload sample crash report to API") { - // Skip if app ID is not configured - if (testAppId == "YOUR_APP_ID_HERE") { - println("\n⚠️ Skipping test: Please set testAppId to a valid app ID") - println(" To run this test, edit the test file and set testAppId to your OneSignal App ID") - return@test - } - - runBlocking { - // Create platform provider with test data from SharedPreferences - val platformProvider = createAndroidOtelPlatformProvider(appContext!!) - - // Verify app ID is set correctly - platformProvider.appId shouldBe testAppId - platformProvider.appIdForHeaders shouldBe testAppId - - // Log platform provider details - val platformDetails = """ - |📋 Platform Provider Details: - | App ID: ${platformProvider.appId} - | App ID for Headers: ${platformProvider.appIdForHeaders} - | SDK Base: ${platformProvider.sdkBase} - | SDK Version: ${platformProvider.sdkBaseVersion} - | App Package: ${platformProvider.appPackageId} - | App Version: ${platformProvider.appVersion} - | Device: ${platformProvider.deviceManufacturer} ${platformProvider.deviceModel} - | OS: ${platformProvider.osName} ${platformProvider.osVersion} - | OneSignal ID: ${platformProvider.onesignalId} - | Push Subscription ID: ${platformProvider.pushSubscriptionId} - | App State: ${platformProvider.appState} - | Remote Log Level: ${platformProvider.remoteLogLevel} - | Install ID: ${runBlocking { platformProvider.getInstallId() }} - """.trimMargin() - println(platformDetails) - Logging.info(platformDetails) - - // Create remote telemetry instance - println("\n🔧 Creating remote telemetry instance...") - val remoteTelemetry: IOtelOpenTelemetryRemote = OtelFactory.createRemoteTelemetry(platformProvider) - remoteTelemetry.shouldBeInstanceOf() - println(" ✅ Remote telemetry created") - - // Create a sample crash report - val sampleException = RuntimeException("Test crash report from integration test") - sampleException.stackTrace = arrayOf( - StackTraceElement("TestClass", "testMethod", "TestFile.kt", 42), - StackTraceElement("TestClass", "anotherMethod", "TestFile.kt", 30), - StackTraceElement("Main", "main", "Main.kt", 10) - ) - - val crashReportInfo = """ - |📤 Uploading crash report to API... - | App ID: ${platformProvider.appId} - | Exception Type: ${sampleException.javaClass.name} - | Exception Message: ${sampleException.message} - | Stack Trace Length: ${sampleException.stackTraceToString().length} chars - | - |📦 Crash Report Payload: - | Level: FATAL - | Message: Sample crash report for API testing - | Exception Type: ${sampleException.javaClass.name} - | Exception Message: ${sampleException.message} - | Stack Trace Preview: ${sampleException.stackTraceToString().take(200)}... - """.trimMargin() - println(crashReportInfo) - Logging.info(crashReportInfo) - - // Use OtelLoggingHelper to send the crash report (this handles all OpenTelemetry internals) - println("\n🚀 Calling OtelLoggingHelper.logToOtel()...") - Logging.info("🚀 Calling OtelLoggingHelper.logToOtel()...") - try { - OtelLoggingHelper.logToOtel( - telemetry = remoteTelemetry, - level = "FATAL", - message = "Sample crash report for API testing", - exceptionType = sampleException.javaClass.name, - exceptionMessage = sampleException.message, - exceptionStacktrace = sampleException.stackTraceToString() - ) - val successMsg = " ✅ logToOtel() completed successfully" - println(successMsg) - Logging.info(successMsg) - } catch (e: Exception) { - val errorMsg = " ❌ Error calling logToOtel(): ${e.message}" - println(errorMsg) - Logging.error(errorMsg, e) - e.printStackTrace() - throw e - } - - // Note: forceFlush() returns CompletableResultCode which is not accessible from core module - // OpenTelemetry will automatically batch and send the logs, so we just wait a bit - println("\n🔄 Waiting for telemetry to be sent (OpenTelemetry batches automatically)...") - println(" Batch delay: 1 second (configured in OtelConfigShared)") - println(" Waiting 5 seconds to ensure batch is sent...") - for (i in 1..5) { - delay(1000) - println(" ⏳ Waited $i second(s)...") - } - - // Note: CompletableResultCode is not directly accessible from core module - // We just wait and assume success if no exception was thrown - println("\n✅ Crash report upload process completed!") - println(" Check your backend dashboard to verify the crash report was received") - println(" Note: OpenTelemetry batches requests, so it may take a moment to appear") - println(" Expected endpoint: https://api.staging.onesignal.com/sdk/otel/v1/logs?app_id=${platformProvider.appId}") - } - } - - test("should upload crash report using OtelLoggingHelper") { - // Skip if app ID is not configured - if (testAppId == "YOUR_APP_ID_HERE") { - println("⚠️ Skipping test: Please set testAppId to a valid app ID") - return@test - } - - runBlocking { - // Create platform provider - val platformProvider = createAndroidOtelPlatformProvider(appContext!!) - - // Create remote telemetry - val remoteTelemetry: IOtelOpenTelemetryRemote = OtelFactory.createRemoteTelemetry(platformProvider) - - // Create sample exception - val sampleException = IllegalStateException("Test exception from OtelLoggingHelper test") - - println("📤 Uploading crash report via OtelLoggingHelper...") - println(" App ID: ${platformProvider.appId}") - - // Use OtelLoggingHelper to send the crash report - OtelLoggingHelper.logToOtel( - telemetry = remoteTelemetry, - level = "FATAL", - message = "Sample crash report via OtelLoggingHelper", - exceptionType = sampleException.javaClass.name, - exceptionMessage = sampleException.message, - exceptionStacktrace = sampleException.stackTraceToString() - ) - - // Note: forceFlush() returns CompletableResultCode which is not accessible from core module - // OpenTelemetry will automatically batch and send the logs, so we just wait a bit - println("🔄 Waiting for telemetry to be sent (OpenTelemetry batches automatically)...") - delay(3000) // Wait 3 seconds for automatic batching to send - - println("✅ Crash report sent via OtelLoggingHelper!") - println(" Check your backend dashboard to verify the crash report was received") - println(" Note: OpenTelemetry batches requests, so it may take a moment to appear") - } - } - - test("should verify platform provider has all required fields for crash report") { - println("\n🔍 Testing Platform Provider Configuration...") - - val platformProvider = createAndroidOtelPlatformProvider(appContext!!) - - println("\n📋 Platform Provider Fields:") - println(" App ID: ${platformProvider.appId}") - println(" App ID for Headers: ${platformProvider.appIdForHeaders}") - println(" SDK Base: ${platformProvider.sdkBase}") - println(" SDK Version: ${platformProvider.sdkBaseVersion}") - println(" App Package: ${platformProvider.appPackageId}") - println(" App Version: ${platformProvider.appVersion}") - println(" Device: ${platformProvider.deviceManufacturer} ${platformProvider.deviceModel}") - println(" OS: ${platformProvider.osName} ${platformProvider.osVersion} (Build: ${platformProvider.osBuildId})") - println(" OneSignal ID: ${platformProvider.onesignalId}") - println(" Push Subscription ID: ${platformProvider.pushSubscriptionId}") - println(" App State: ${platformProvider.appState}") - println(" Process Uptime: ${platformProvider.processUptime}ms") - println(" Thread Name: ${platformProvider.currentThreadName}") - println(" Remote Log Level: ${platformProvider.remoteLogLevel}") - - runBlocking { - val installId = platformProvider.getInstallId() - println(" Install ID: $installId") - installId shouldNotBe null - installId.isNotEmpty() shouldBe true - } - - // Verify all required fields are present - platformProvider.appId shouldNotBe null - platformProvider.appIdForHeaders shouldNotBe null - platformProvider.sdkBase shouldBe "android" - platformProvider.sdkBaseVersion shouldNotBe null - platformProvider.appPackageId shouldBe appContext!!.packageName // Use actual package name from context - platformProvider.appVersion shouldNotBe null - platformProvider.deviceManufacturer shouldNotBe null - platformProvider.deviceModel shouldNotBe null - platformProvider.osName shouldBe "Android" - platformProvider.osVersion shouldNotBe null - platformProvider.osBuildId shouldNotBe null - - println("\n✅ All platform provider fields verified!") - - // Show what would be sent in a crash report - println("\n📦 Sample Crash Report Attributes (what would be sent):") - println(" Top-Level (Resource):") - println(" - service.name: OneSignalDeviceSDK") - println(" - ossdk.install_id: ${runBlocking { platformProvider.getInstallId() }}") - println(" - ossdk.sdk_base: ${platformProvider.sdkBase}") - println(" - ossdk.sdk_base_version: ${platformProvider.sdkBaseVersion}") - println(" - ossdk.app_package_id: ${platformProvider.appPackageId}") - println(" - ossdk.app_version: ${platformProvider.appVersion}") - println(" - device.manufacturer: ${platformProvider.deviceManufacturer}") - println(" - device.model.identifier: ${platformProvider.deviceModel}") - println(" - os.name: ${platformProvider.osName}") - println(" - os.version: ${platformProvider.osVersion}") - println(" - os.build_id: ${platformProvider.osBuildId}") - println(" Per-Event:") - println(" - log.record.uid: ") - println(" - ossdk.app_id: ${platformProvider.appId}") - println(" - ossdk.onesignal_id: ${platformProvider.onesignalId}") - println(" - ossdk.push_subscription_id: ${platformProvider.pushSubscriptionId}") - println(" - app.state: ${platformProvider.appState}") - println(" - process.uptime: ${platformProvider.processUptime}") - println(" - thread.name: ${platformProvider.currentThreadName}") - println(" Log-Specific:") - println(" - log.message: ") - println(" - log.level: FATAL") - println(" - exception.type: ") - println(" - exception.message: ") - println(" - exception.stacktrace: ") - println("\n Expected Endpoint: https://api.staging.onesignal.com/sdk/otel/v1/logs?app_id=${platformProvider.appId}") - } -})