Skip to content
Merged
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
4 changes: 4 additions & 0 deletions OneSignalSDK/detekt/detekt-baseline-core.xml
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,7 @@
<ID>ConstructorParameterNaming:UpdateUserOperationExecutor.kt$UpdateUserOperationExecutor$private val _propertiesModelStore: PropertiesModelStore</ID>
<ID>ConstructorParameterNaming:UpdateUserOperationExecutor.kt$UpdateUserOperationExecutor$private val _userBackend: IUserBackendService</ID>
<ID>ConstructorParameterNaming:UserBackendService.kt$UserBackendService$private val _httpClient: IHttpClient</ID>
<ID>ConstructorParameterNaming:UserManager.kt$UserManager$private val _customEventController: ICustomEventController</ID>
<ID>ConstructorParameterNaming:UserManager.kt$UserManager$private val _identityModelStore: IdentityModelStore</ID>
<ID>ConstructorParameterNaming:UserManager.kt$UserManager$private val _languageContext: ILanguageContext</ID>
<ID>ConstructorParameterNaming:UserManager.kt$UserManager$private val _propertiesModelStore: PropertiesModelStore</ID>
Expand Down Expand Up @@ -191,6 +192,7 @@
<ID>LongMethod:TrackGooglePurchase.kt$TrackGooglePurchase$private fun queryBoughtItems()</ID>
<ID>LongMethod:TrackGooglePurchase.kt$TrackGooglePurchase$private fun sendPurchases( skusToAdd: ArrayList&lt;String>, newPurchaseTokens: ArrayList&lt;String>, )</ID>
<ID>LongMethod:UpdateUserOperationExecutor.kt$UpdateUserOperationExecutor$override suspend fun execute(operations: List&lt;Operation>): ExecutionResponse</ID>
<ID>LongParameterList:ICustomEventBackendService.kt$ICustomEventBackendService$( appId: String, onesignalId: String, externalId: String?, timestamp: Long, eventName: String, eventProperties: String?, metadata: CustomEventMetadata, )</ID>
<ID>LongParameterList:IDatabase.kt$IDatabase$( table: String, columns: Array&lt;String>? = null, whereClause: String? = null, whereArgs: Array&lt;String>? = null, groupBy: String? = null, having: String? = null, orderBy: String? = null, limit: String? = null, action: (ICursor) -> Unit, )</ID>
<ID>LongParameterList:IOutcomeEventsBackendService.kt$IOutcomeEventsBackendService$( appId: String, userId: String, subscriptionId: String, deviceType: String, direct: Boolean?, event: OutcomeEvent, )</ID>
<ID>LongParameterList:IParamsBackendService.kt$ParamsObject$( var googleProjectNumber: String? = null, var enterprise: Boolean? = null, var useIdentityVerification: Boolean? = null, var notificationChannels: JSONArray? = null, var firebaseAnalytics: Boolean? = null, var restoreTTLFilter: Boolean? = null, var clearGroupOnSummaryClick: Boolean? = null, var receiveReceiptEnabled: Boolean? = null, var disableGMSMissingPrompt: Boolean? = null, var unsubscribeWhenNotificationsDisabled: Boolean? = null, var locationShared: Boolean? = null, var requiresUserPrivacyConsent: Boolean? = null, var opRepoExecutionInterval: Long? = null, var influenceParams: InfluenceParamsObject, var fcmParams: FCMParamsObject, )</ID>
Expand Down Expand Up @@ -279,6 +281,7 @@
<ID>RethrowCaughtException:OSDatabase.kt$OSDatabase$throw e</ID>
<ID>ReturnCount:AppIdResolution.kt$fun resolveAppId( inputAppId: String?, configModel: ConfigModel, preferencesService: IPreferencesService, ): AppIdResolution</ID>
<ID>ReturnCount:BackgroundManager.kt$BackgroundManager$override fun cancelRunBackgroundServices(): Boolean</ID>
<ID>ReturnCount:OneSignalImp.kt$OneSignalImp$private fun internalInit( context: Context, appId: String?, ): Boolean</ID>
<ID>ReturnCount:ConfigModel.kt$ConfigModel$override fun createModelForProperty( property: String, jsonObject: JSONObject, ): Model?</ID>
<ID>ReturnCount:HttpClient.kt$HttpClient$private suspend fun makeRequest( url: String, method: String?, jsonBody: JSONObject?, timeout: Int, headers: OptionalHeaders?, ): HttpResponse</ID>
<ID>ReturnCount:IdentityOperationExecutor.kt$IdentityOperationExecutor$override suspend fun execute(operations: List&lt;Operation>): ExecutionResponse</ID>
Expand Down Expand Up @@ -370,6 +373,7 @@
<ID>TooManyFunctions:IUserManager.kt$IUserManager</ID>
<ID>TooManyFunctions:InfluenceManager.kt$InfluenceManager : IInfluenceManagerISessionLifecycleHandler</ID>
<ID>TooManyFunctions:JSONObjectExtensions.kt$com.onesignal.common.JSONObjectExtensions.kt</ID>
<ID>TooManyFunctions:JSONUtils.kt$JSONUtils$JSONUtils</ID>
<ID>TooManyFunctions:Logging.kt$Logging$Logging</ID>
<ID>TooManyFunctions:Model.kt$Model : IEventNotifier</ID>
<ID>TooManyFunctions:ModelStore.kt$ModelStore&lt;TModel> : IEventNotifierIModelStoreIModelChangedHandler</ID>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,4 +58,5 @@ internal class FCMParamsObject(

internal class RemoteLoggingParamsObject(
val logLevel: com.onesignal.debug.LogLevel? = null,
val isEnabled: Boolean = logLevel != null,
)
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ internal class ParamsBackendService(
// Process Remote Logging params
var remoteLoggingParams: RemoteLoggingParamsObject? = null
responseJson.expandJSONObject("logging_config") {
val logLevel = parseLogLevel(it)
val logLevel = LogLevel.fromString(it.safeString("log_level"))
remoteLoggingParams =
RemoteLoggingParamsObject(
logLevel = logLevel,
Expand Down Expand Up @@ -134,38 +134,4 @@ internal class ParamsBackendService(
isUnattributedEnabled,
)
}

/**
* Parse LogLevel from JSON. Supports both string (enum name) and int (ordinal) formats.
*/
@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) {
try {
return LogLevel.valueOf(logLevelString.uppercase())
} catch (e: IllegalArgumentException) {
Logging.warn("Invalid log level string: $logLevelString")
}
}

// 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
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.onesignal.core.internal.config

import com.onesignal.common.modeling.Model
import com.onesignal.core.internal.http.OneSignalService.ONESIGNAL_API_BASE_URL
import org.json.JSONArray
import org.json.JSONObject

Expand Down Expand Up @@ -36,7 +37,7 @@ class ConfigModel : Model() {
* The API URL String.
*/
var apiUrl: String
get() = getStringProperty(::apiUrl.name) { "https://api.onesignal.com/" }
get() = getStringProperty(::apiUrl.name) { ONESIGNAL_API_BASE_URL }
set(value) {
setStringProperty(::apiUrl.name, value)
}
Expand Down Expand Up @@ -454,4 +455,14 @@ class RemoteLoggingConfigModel(
set(value) {
setOptEnumProperty(::logLevel.name, value)
}

/**
* Whether remote logging is enabled.
* Set by backend config hydration — true when the server sends a valid log_level, false otherwise.
*/
var isEnabled: Boolean
get() = getBooleanProperty(::isEnabled.name) { false }
set(value) {
setBooleanProperty(::isEnabled.name, value)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,7 @@ internal class ConfigModelStoreListener(
params.influenceParams.isUnattributedEnabled?.let { config.influenceParams.isUnattributedEnabled = it }

params.remoteLoggingParams.logLevel?.let { config.remoteLoggingParams.logLevel = it }
config.remoteLoggingParams.isEnabled = params.remoteLoggingParams.isEnabled

_configModelStore.replace(config, ModelChangeTags.HYDRATE)
success = true
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package com.onesignal.core.internal.http

/** Central API base URL used by all SDK HTTP traffic, including Otel log export. */
object OneSignalService {
// const val ONESIGNAL_API_BASE_URL = "https://api.staging.onesignal.com/"
const val ONESIGNAL_API_BASE_URL = "https://api.onesignal.com/"
}
Original file line number Diff line number Diff line change
Expand Up @@ -49,5 +49,19 @@ enum class LogLevel {
fun fromInt(value: Int): LogLevel {
return values()[value]
}

/**
* Parses a [LogLevel] from its string name (case-insensitive).
* Returns `null` if the string is null or not a valid level name.
*/
@JvmStatic
fun fromString(value: String?): LogLevel? {
if (value == null) return null
return try {
valueOf(value.uppercase())
} catch (_: IllegalArgumentException) {
null
}
}
}
}
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.
* This should be called as early as possible, before any other initialization.
* Creates an Otel crash handler. Must only be called on supported devices
* (SDK >= [OtelSdkSupport.MIN_SDK_VERSION]).
*
* @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
@@ -1,12 +1,12 @@
package com.onesignal.debug.internal.crash

import com.onesignal.common.threading.OneSignalDispatchers
import com.onesignal.core.internal.application.IApplicationService
import com.onesignal.core.internal.startup.IStartableService
import com.onesignal.debug.internal.logging.otel.android.AndroidOtelLogger
import com.onesignal.debug.internal.logging.otel.android.createAndroidOtelPlatformProvider
import com.onesignal.otel.OtelFactory
import com.onesignal.otel.crash.OtelCrashUploader
import kotlinx.coroutines.runBlocking

/**
* Android-specific wrapper for OtelCrashUploader that implements IStartableService.
Expand Down Expand Up @@ -43,9 +43,18 @@ internal class OneSignalCrashUploaderWrapper(
OtelFactory.createCrashUploader(platformProvider, logger)
}

@Suppress("TooGenericExceptionCaught")
override fun start() {
runBlocking {
uploader.start()
if (!OtelSdkSupport.isSupported) return
OneSignalDispatchers.launchOnIO {
try {
uploader.start()
} catch (t: Throwable) {
com.onesignal.debug.internal.logging.Logging.warn(
"OneSignal: Crash uploader failed to start: ${t.message}",
t,
)
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ import java.util.concurrent.atomic.AtomicLong
* It creates its own crash reporter to save ANR reports.
*/
internal class OtelAnrDetector(
private val openTelemetryCrash: IOtelOpenTelemetryCrash,
openTelemetryCrash: IOtelOpenTelemetryCrash,
private val logger: IOtelLogger,
private val anrThresholdMs: Long = AnrConstants.DEFAULT_ANR_THRESHOLD_MS,
private val checkIntervalMs: Long = AnrConstants.DEFAULT_CHECK_INTERVAL_MS,
Expand Down Expand Up @@ -58,6 +58,7 @@ internal class OtelAnrDetector(
logger.info("$TAG: ✅ ANR detection started successfully")
}

@Suppress("TooGenericExceptionCaught")
private fun setupRunnables() {
// Runnable that runs on the main thread to indicate it's responsive
mainThreadRunnable = Runnable {
Expand All @@ -73,17 +74,16 @@ internal class OtelAnrDetector(
// Thread was interrupted, stop monitoring
logger.info("$TAG: Watchdog thread interrupted, stopping ANR detection")
break
} catch (e: RuntimeException) {
logger.error("$TAG: Error in ANR watchdog: ${e.message} - ${e.javaClass.simpleName}: ${e.stackTraceToString()}")
// Continue monitoring even if there's an error
} catch (t: Throwable) {
logger.error("$TAG: Error in ANR watchdog: ${t.message} - ${t.javaClass.simpleName}")
}
}
}
}

private fun checkForAnr() {
// Post a message to the main thread
mainHandler.post(mainThreadRunnable!!)
val runnable = mainThreadRunnable ?: return
mainHandler.post(runnable)

// Wait for the check interval
Thread.sleep(checkIntervalMs)
Expand Down Expand Up @@ -146,6 +146,7 @@ internal class OtelAnrDetector(
logger.info("$TAG: ✅ ANR detection stopped")
}

@Suppress("TooGenericExceptionCaught")
private fun reportAnr(unresponsiveDurationMs: Long) {
try {
logger.info("$TAG: Checking if ANR is OneSignal-related (unresponsive for ${unresponsiveDurationMs}ms)")
Expand Down Expand Up @@ -176,8 +177,8 @@ internal class OtelAnrDetector(
}

logger.info("$TAG: ✅ ANR report saved successfully")
} catch (e: RuntimeException) {
logger.error("$TAG: Failed to report ANR: ${e.message} - ${e.javaClass.simpleName}: ${e.stackTraceToString()}")
} catch (t: Throwable) {
logger.error("$TAG: Failed to report ANR: ${t.message} - ${t.javaClass.simpleName}")
}
}

Expand Down
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 @@ -228,10 +228,9 @@ object Logging {
exceptionMessage = throwable?.message,
exceptionStacktrace = throwable?.stackTraceToString(),
)
} catch (e: Exception) {
} catch (t: Throwable) {
// Don't log Otel errors to Otel (would cause infinite loop)
// Just log to logcat silently
android.util.Log.e(TAG, "Failed to log to Otel: ${e.message}", e)
android.util.Log.e(TAG, "Failed to log to Otel: ${t.message}", t)
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,23 @@ internal class OtelIdResolver(
}
}

/**
* Resolves whether remote logging is enabled from cached ConfigModelStore.
* Enabled is derived from the presence of a valid logLevel:
* - "logging_config": {} → no logLevel → disabled (not on allowlist)
* - "logging_config": {"log_level": "ERROR"} → has logLevel → enabled (on allowlist)
* Returns false if not found, empty, or on error (disabled by default on first launch).
*/
@Suppress("TooGenericExceptionCaught", "SwallowedException")
fun resolveRemoteLoggingEnabled(): Boolean {
return try {
val logLevel = resolveRemoteLogLevel()
logLevel != null && logLevel != com.onesignal.debug.LogLevel.NONE
} catch (e: Exception) {
false
}
}

/**
* Resolves remote log level from cached ConfigModelStore.
* Returns null if not found or if there's an error.
Expand All @@ -209,19 +226,10 @@ internal class OtelIdResolver(
}
}

@Suppress("TooGenericExceptionCaught", "SwallowedException")
private fun extractLogLevelFromParams(remoteLoggingParams: JSONObject): com.onesignal.debug.LogLevel? {
return if (remoteLoggingParams.has("logLevel")) {
val logLevelString = remoteLoggingParams.getString("logLevel")
try {
com.onesignal.debug.LogLevel.valueOf(logLevelString.uppercase())
} catch (e: Exception) {
null
}
} else {
null
}
}
private fun extractLogLevelFromParams(remoteLoggingParams: JSONObject): com.onesignal.debug.LogLevel? =
com.onesignal.debug.LogLevel.fromString(
if (remoteLoggingParams.has("logLevel")) remoteLoggingParams.getString("logLevel") else null
)

/**
* Resolves install ID from SharedPreferences.
Expand Down
Loading
Loading