Skip to content
Merged
Show file tree
Hide file tree
Changes from 22 commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
e5b4e4f
feat: base parts to send otel crash reports
jkasten2 Nov 3, 2025
483c8d1
feat: disk buffering for Otel
jkasten2 Nov 5, 2025
3a60cef
feat: Otel crash reports log to disk
jkasten2 Nov 5, 2025
04a620c
feat: add top level fields
jkasten2 Nov 5, 2025
17dcf00
feat: add Otel shared per event fields
jkasten2 Nov 5, 2025
052ddfa
chore: improved names and lint fixes
jkasten2 Nov 6, 2025
38ba6fb
feat: Add idempotency key to Otel events
jkasten2 Nov 6, 2025
fc0d830
fix: update HTTP headers sent to otel endpoints
jkasten2 Nov 6, 2025
f7ab8a0
fix: start crash handler sooner
jkasten2 Nov 17, 2025
a61e2bc
fix: error handling around models
jkasten2 Nov 17, 2025
597b858
feat: add remote param for remote logging
jkasten2 Dec 11, 2025
b10757a
Merge remote-tracking branch 'refs/remotes/origin/main' into ar-otel-…
Dec 28, 2025
76460ee
decoupled and modularized
Dec 28, 2025
24c7c33
WIP: Save current state before merging main
Dec 28, 2025
55b0d2e
Merge latest changes from main
Dec 28, 2025
90be915
detekt fixes and gradle cleanup
Dec 28, 2025
f2eb2c0
incrementing kotling version
Dec 28, 2025
c9a7380
added documentation
Dec 28, 2025
41f0e7a
offload init background. lambda based
Dec 29, 2025
353ac95
reusing lambdas
Dec 29, 2025
68d572c
wrote more tests, refactored the code to make it more testable
Jan 6, 2026
2cfbfba
hardcoded APP Ids and json serialize values
Jan 28, 2026
6ebd305
chore: Logging lower severity (#2531)
jkasten2 Jan 29, 2026
3d9021e
chore: Cleaned up and Added new tests (#2541)
abdulraqeeb33 Feb 9, 2026
1bc6ca8
remove callbacks
Feb 9, 2026
1ac7eb6
Merge branch 'main' into ar-otel-crash-reporting
Feb 24, 2026
8f8ee0b
feat: logging_config-based remote logging control, SDK version gating…
abdulraqeeb33 Mar 3, 2026
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 @@ -41,7 +41,11 @@ import com.onesignal.user.state.UserState
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import java.text.SimpleDateFormat
import java.util.Date
import java.util.Locale

class MainApplicationKT : MultiDexApplication() {

Expand Down Expand Up @@ -80,9 +84,43 @@ class MainApplicationKT : MultiDexApplication() {
OneSignal.Notifications.requestPermission(true)

Log.d(Tag.LOG_TAG, Text.ONESIGNAL_SDK_INIT)

delay(3000)
}
// crashApp()
// forceANR()
}

private fun forceANR() {
try {
android.os.Handler(android.os.Looper.getMainLooper()).post {
Log.d(Tag.LOG_TAG, "Starting infinite loop on main thread to trigger ANR.")
// This will block the main thread indefinitely, triggering an ANR
// The ANR detector will detect it after 5 seconds if OneSignal code is in the stack trace
while (true) {
Log.d(Tag.LOG_TAG, "Blocking main thread - ANR test")
// Small sleep to prevent excessive CPU usage, but still blocks the thread
Thread.sleep(100)
}
}
} catch (e: InterruptedException) {
e.printStackTrace()
}
}

private fun crashApp() {
val sdf = SimpleDateFormat(
"MMM dd, yyyy HH:mm:ss",
Locale.getDefault()
)

crashApp()
val currentTimeMillis = System.currentTimeMillis()
val date = Date(currentTimeMillis)
val formattedDate = sdf.format(date)
throw RuntimeException("test crash from AR $formattedDate")
}

private fun setupOneSignalListeners() {
OneSignal.InAppMessages.addLifecycleListener(object : IInAppMessageLifecycleListener {
override fun onWillDisplay(@NonNull event: IInAppMessageWillDisplayEvent) {
Expand Down
16 changes: 9 additions & 7 deletions OneSignalSDK/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,8 @@ buildscript {
huaweiAgconnectVersion = '1.9.1.304'
huaweiHMSPushVersion = '6.3.0.304'
huaweiHMSLocationVersion = '4.0.0.300'
kotlinVersion = '1.9.25'
dokkaVersion = '1.9.10' // Dokka version compatible with Kotlin 1.9.25
kotlinVersion = '2.2.0'
dokkaVersion = '1.9.10'
coroutinesVersion = '1.7.3'
kotestVersion = '5.8.0'
ioMockVersion = '1.13.2'
Expand All @@ -25,6 +25,10 @@ buildscript {
ktlintVersion = '0.50.0' // Used by Spotless for Kotlin formatting (compatible with Kotlin 1.7.10)
spotlessVersion = '6.25.0'
tdunningJsonForTest = '1.0' // DO NOT upgrade for tests, using an old version so it matches AOSP
// OpenTelemetry versions
opentelemetryBomVersion = '1.55.0'
opentelemetrySemconvVersion = '1.37.0'
opentelemetryDiskBufferingVersion = '1.51.0-alpha'

sharedRepos = {
google()
Expand All @@ -45,11 +49,9 @@ buildscript {
]
}

buildscript {
repositories sharedRepos
dependencies {
classpath sharedDeps
}
repositories sharedRepos
dependencies {
classpath sharedDeps
}
}

Expand Down
2 changes: 1 addition & 1 deletion OneSignalSDK/detekt/detekt-config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ comments:
UndocumentedPublicFunction:
active: true
excludes: ['**/test/**', '**/androidTest/**', '**/testhelpers/**']

EndOfSentenceFormat:
active: false
endOfSentenceFormat: ([.?!][ \t\n\r\f<])|([.?!:]$)
Expand Down
2 changes: 2 additions & 0 deletions OneSignalSDK/onesignal/core/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,8 @@ dependencies {
}
}

// Otel module dependency
implementation(project(':OneSignal:otel'))
testImplementation(project(':OneSignal:testhelpers'))

testImplementation("io.kotest:kotest-runner-junit5:$kotestVersion")
Expand Down
7 changes: 6 additions & 1 deletion OneSignalSDK/onesignal/core/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">

<!-- Override otel module's minSdk requirement (26) since we have runtime checks -->
<!-- The otel module is only used on SDK 26+, so this is safe -->
<uses-sdk tools:overrideLibrary="com.onesignal.otel" />

<!-- Required so the device can access the internet. -->
<uses-permission android:name="android.permission.INTERNET" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ import com.onesignal.core.internal.purchases.impl.TrackGooglePurchase
import com.onesignal.core.internal.startup.IStartableService
import com.onesignal.core.internal.time.ITime
import com.onesignal.core.internal.time.impl.Time
import com.onesignal.debug.internal.crash.OneSignalCrashUploaderWrapper
import com.onesignal.inAppMessages.IInAppMessagesManager
import com.onesignal.inAppMessages.internal.MisconfiguredIAMManager
import com.onesignal.location.ILocationManager
Expand Down Expand Up @@ -81,6 +82,9 @@ internal class CoreModule : IModule {
// Purchase Tracking
builder.register<TrackGooglePurchase>().provides<IStartableService>()

// Crash Uploader (crash handler is initialized directly in OneSignalImp for early initialization)
builder.register<OneSignalCrashUploaderWrapper>().provides<IStartableService>()

// Register dummy services in the event they are not configured. These dummy services
// will throw an error message if the associated functionality is attempted to be used.
builder.register<MisconfiguredNotificationsManager>().provides<INotificationsManager>()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ package com.onesignal.core.internal.backend

import org.json.JSONArray

interface IParamsBackendService {
internal interface IParamsBackendService {
/**
* Retrieve the configuration parameters for the [appId] and optional [subscriptionId].
*
Expand All @@ -20,7 +20,8 @@ interface IParamsBackendService {
): ParamsObject
}

class ParamsObject(
@Suppress("LongParameterList")
internal class ParamsObject(
var googleProjectNumber: String? = null,
var enterprise: Boolean? = null,
var useIdentityVerification: Boolean? = null,
Expand All @@ -36,9 +37,10 @@ class ParamsObject(
var opRepoExecutionInterval: Long? = null,
var influenceParams: InfluenceParamsObject,
var fcmParams: FCMParamsObject,
val remoteLoggingParams: RemoteLoggingParamsObject,
)

class InfluenceParamsObject(
internal class InfluenceParamsObject(
val indirectNotificationAttributionWindow: Int? = null,
val notificationLimit: Int? = null,
val indirectIAMAttributionWindow: Int? = null,
Expand All @@ -48,8 +50,12 @@ class InfluenceParamsObject(
val isUnattributedEnabled: Boolean? = null,
)

class FCMParamsObject(
internal class FCMParamsObject(
val projectId: String? = null,
val appId: String? = null,
val apiKey: String? = null,
)

internal class RemoteLoggingParamsObject(
val logLevel: com.onesignal.debug.LogLevel? = null,
)
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import com.onesignal.core.internal.backend.FCMParamsObject
import com.onesignal.core.internal.backend.IParamsBackendService
import com.onesignal.core.internal.backend.InfluenceParamsObject
import com.onesignal.core.internal.backend.ParamsObject
import com.onesignal.core.internal.backend.RemoteLoggingParamsObject
import com.onesignal.core.internal.http.CacheKeys
import com.onesignal.core.internal.http.IHttpClient
import com.onesignal.core.internal.http.impl.OptionalHeaders
Expand Down Expand Up @@ -57,6 +58,16 @@ internal class ParamsBackendService(
)
}

// Process Remote Logging params
var remoteLoggingParams: RemoteLoggingParamsObject? = null
responseJson.expandJSONObject("logging_config") {
val logLevel = parseLogLevel(it)
remoteLoggingParams =
RemoteLoggingParamsObject(
logLevel = logLevel,
)
}

return ParamsObject(
googleProjectNumber = responseJson.safeString("android_sender_id"),
enterprise = responseJson.safeBool("enterp"),
Expand All @@ -75,6 +86,7 @@ internal class ParamsBackendService(
opRepoExecutionInterval = responseJson.safeLong("oprepo_execution_interval"),
influenceParams = influenceParams ?: InfluenceParamsObject(),
fcmParams = fcmParams ?: FCMParamsObject(),
remoteLoggingParams = remoteLoggingParams ?: RemoteLoggingParamsObject(),
)
}

Expand Down Expand Up @@ -122,4 +134,38 @@ 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
Expand Up @@ -301,6 +301,9 @@ class ConfigModel : Model() {
val fcmParams: FCMConfigModel
get() = getAnyProperty(::fcmParams.name) { FCMConfigModel(this, ::fcmParams.name) } as FCMConfigModel

val remoteLoggingParams: RemoteLoggingConfigModel
get() = getAnyProperty(::remoteLoggingParams.name) { RemoteLoggingConfigModel(this, ::remoteLoggingParams.name) } as RemoteLoggingConfigModel

override fun createModelForProperty(
property: String,
jsonObject: JSONObject,
Expand All @@ -317,6 +320,12 @@ class ConfigModel : Model() {
return model
}

if (property == ::remoteLoggingParams.name) {
val model = RemoteLoggingConfigModel(this, ::remoteLoggingParams.name)
model.initializeFromJson(jsonObject)
return model
}

return null
}
}
Expand Down Expand Up @@ -425,3 +434,24 @@ class FCMConfigModel(parentModel: Model, parentProperty: String) : Model(parentM
setOptStringProperty(::apiKey.name, value)
}
}

/**
* Configuration related to OneSignal's remote logging.
*/
class RemoteLoggingConfigModel(
parentModel: Model,
parentProperty: String,
) : Model(parentModel, parentProperty) {
/**
* The minimum log level to send to OneSignal's server.
* If null, defaults to ERROR level for client-side logging.
* If NONE, no logs (including errors) will be sent remotely.
*
* Log levels: NONE < FATAL < ERROR < WARN < INFO < DEBUG < VERBOSE
*/
var logLevel: com.onesignal.debug.LogLevel?
get() = getOptEnumProperty<com.onesignal.debug.LogLevel>(::logLevel.name)
set(value) {
setOptEnumProperty(::logLevel.name, value)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@ package com.onesignal.core.internal.config
import com.onesignal.common.modeling.SimpleModelStore
import com.onesignal.common.modeling.SingletonModelStore
import com.onesignal.core.internal.preferences.IPreferencesService
const val CONFIG_NAME_SPACE = "config"

open class ConfigModelStore(prefs: IPreferencesService) : SingletonModelStore<ConfigModel>(
SimpleModelStore({ ConfigModel() }, "config", prefs),
SimpleModelStore({ ConfigModel() }, CONFIG_NAME_SPACE, prefs),
)
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,8 @@ internal class ConfigModelStoreListener(
params.influenceParams.isIndirectEnabled?.let { config.influenceParams.isIndirectEnabled = it }
params.influenceParams.isUnattributedEnabled?.let { config.influenceParams.isUnattributedEnabled = it }

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

_configModelStore.replace(config, ModelChangeTags.HYDRATE)
success = true
} catch (ex: BackendException) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,9 @@ import java.net.UnknownHostException
import java.util.Scanner
import javax.net.ssl.HttpsURLConnection

internal const val HTTP_SDK_VERSION_HEADER_KEY = "SDK-Version"
internal val HTTP_SDK_VERSION_HEADER_VALUE = "onesignal/android/${OneSignalUtils.sdkVersion}"

internal class HttpClient(
private val _connectionFactory: IHttpConnectionFactory,
private val _prefs: IPreferencesService,
Expand Down Expand Up @@ -131,7 +134,7 @@ internal class HttpClient(
con.useCaches = false
con.connectTimeout = timeout
con.readTimeout = timeout
con.setRequestProperty("SDK-Version", "onesignal/android/" + OneSignalUtils.sdkVersion)
con.setRequestProperty(HTTP_SDK_VERSION_HEADER_KEY, HTTP_SDK_VERSION_HEADER_VALUE)

if (OneSignalWrapper.sdkType != null && OneSignalWrapper.sdkVersion != null) {
con.setRequestProperty("SDK-Wrapper", "onesignal/${OneSignalWrapper.sdkType}/${OneSignalWrapper.sdkVersion}")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,9 @@ interface ITime {
* current time and midnight, January 1, 1970 UTC).
*/
val currentTimeMillis: Long

/**
* Returns how long the app has been running.
*/
val processUptimeMillis: Long
}
Original file line number Diff line number Diff line change
@@ -1,8 +1,14 @@
package com.onesignal.core.internal.time.impl

import android.os.Build
import android.os.SystemClock
import androidx.annotation.RequiresApi
import com.onesignal.core.internal.time.ITime

internal class Time : ITime {
override val currentTimeMillis: Long
get() = System.currentTimeMillis()
override val processUptimeMillis: Long
@RequiresApi(Build.VERSION_CODES.N)
get() = SystemClock.uptimeMillis() - android.os.Process.getStartUptimeMillis()
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package com.onesignal.debug.internal.crash

/**
* Constants for ANR (Application Not Responding) detection configuration.
*/
internal object AnrConstants {
/**
* Default ANR threshold in milliseconds.
* Android's default ANR threshold is 5 seconds (5000ms).
* An ANR is reported when the main thread is unresponsive for this duration.
*/
const val DEFAULT_ANR_THRESHOLD_MS: Long = 5_000L

/**
* Default check interval in milliseconds.
* The ANR detector checks the main thread responsiveness every 2 seconds.
*/
const val DEFAULT_CHECK_INTERVAL_MS: Long = 2_000L
}
Loading
Loading