Skip to content
Closed
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
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
private fun parseLogLevel(json: JSONObject): LogLevel {
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
return LogLevel.NONE
}
}
Original file line number Diff line number Diff line change
@@ -1,51 +1,38 @@
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
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)")
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package com.onesignal.debug.internal.crash

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
Expand Down Expand Up @@ -44,8 +45,14 @@ internal class OneSignalCrashUploaderWrapper(
}

override fun start() {
if (!OtelSdkSupport.isSupported) return

runBlocking {
uploader.start()
try {
uploader.start()
} catch (t: Throwable) {
Logging.error("Error attempting to upload crash log", t)
}
}
}
}
Original file line number Diff line number Diff line change
@@ -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
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ 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
Expand Down Expand Up @@ -217,6 +218,8 @@ object Logging {
// Check if this log level should be sent remotely
if (!shouldSendLogLevel(level)) return

if (!OtelSdkSupport.isSupported) return

// Log asynchronously (non-blocking)
otelLoggingScope.launch {
try {
Expand All @@ -228,9 +231,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)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -70,49 +69,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.NONE
}

// 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
else -> level >= remoteLogLevel // Send at configured level and above
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)
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -453,7 +453,7 @@ internal class OneSignalImp(
private fun <T> blockingGet(getter: () -> T): T {
try {
if (AndroidUtils.isRunningOnMainThread()) {
Logging.warn("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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import br.com.colman.kotest.android.extensions.robolectric.RobolectricTest
import com.onesignal.debug.internal.logging.otel.android.AndroidOtelLogger
import com.onesignal.otel.IOtelCrashHandler
import com.onesignal.otel.IOtelLogger
import io.kotest.assertions.throwables.shouldThrow
import io.kotest.core.spec.style.FunSpec
import io.kotest.matchers.shouldNotBe
import io.kotest.matchers.types.shouldBeInstanceOf
Expand All @@ -18,7 +19,6 @@ import org.robolectric.annotation.Config
class OneSignalCrashHandlerFactoryTest : FunSpec({
lateinit var appContext: Context
lateinit var logger: AndroidOtelLogger
// Save original handler to restore after tests
val originalHandler: Thread.UncaughtExceptionHandler? = Thread.getDefaultUncaughtExceptionHandler()

beforeAny {
Expand All @@ -27,24 +27,34 @@ class OneSignalCrashHandlerFactoryTest : FunSpec({
}

afterEach {
// Restore original uncaught exception handler after each test
Thread.setDefaultUncaughtExceptionHandler(originalHandler)
OtelSdkSupport.reset()
}

test("createCrashHandler should return IOtelCrashHandler") {
OtelSdkSupport.isSupported = true
val handler = OneSignalCrashHandlerFactory.createCrashHandler(appContext, logger)

handler.shouldBeInstanceOf<IOtelCrashHandler>()
}

test("createCrashHandler should create handler that can be initialized") {
OtelSdkSupport.isSupported = true
val handler = OneSignalCrashHandlerFactory.createCrashHandler(appContext, logger)

handler shouldNotBe null
handler.initialize()
}

test("createCrashHandler should throw when SDK is unsupported") {
OtelSdkSupport.isSupported = false
shouldThrow<IllegalArgumentException> {
OneSignalCrashHandlerFactory.createCrashHandler(appContext, logger)
}
}

test("createCrashHandler should accept mock logger") {
OtelSdkSupport.isSupported = true
val mockLogger = mockk<IOtelLogger>(relaxed = true)

val handler = OneSignalCrashHandlerFactory.createCrashHandler(appContext, mockLogger)
Expand All @@ -54,15 +64,17 @@ class OneSignalCrashHandlerFactoryTest : FunSpec({
}

test("handler should be idempotent when initialized multiple times") {
OtelSdkSupport.isSupported = true
val handler = OneSignalCrashHandlerFactory.createCrashHandler(appContext, logger)

handler.initialize()
handler.initialize() // Should not throw
handler.initialize()

handler shouldNotBe null
}

test("createCrashHandler should work with different contexts") {
OtelSdkSupport.isSupported = true
val context1: Context = ApplicationProvider.getApplicationContext()
val context2: Context = ApplicationProvider.getApplicationContext()

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
}

Expand Down
Loading
Loading