Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
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
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
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 @@ -61,7 +61,7 @@ object JSONUtils {
try {
val value = jsonObject.opt(key)
if (value is JSONArray || value is JSONObject) {
Logging.error("Omitting key '$key'! sendTags DO NOT supported nested values!")
Logging.warn("Omitting key '$key'! sendTags DO NOT supported nested values!")
} else if (jsonObject.isNull(key) || "" == value) {
result[key] = ""
} else {
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,13 @@ 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,
val isEnabled: Boolean = 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 = LogLevel.fromString(it.safeString("log_level"))
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
Original file line number Diff line number Diff line change
Expand Up @@ -186,7 +186,7 @@ internal class BackgroundManager(
} catch (e: NullPointerException) {
// Catch for buggy Oppo devices
// https://github.com/OneSignal/OneSignal-Android-SDK/issues/487
Logging.error(
Logging.info(
"scheduleSyncServiceAsJob called JobScheduler.jobScheduler which " +
"triggered an internal null Android error. Skipping job.",
e,
Expand Down
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 @@ -301,6 +302,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 +321,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 +435,34 @@ 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)
}

/**
* 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 @@ -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,9 @@ 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 }
config.remoteLoggingParams.isEnabled = params.remoteLoggingParams.isEnabled

_configModelStore.replace(config, ModelChangeTags.HYDRATE)
success = true
} catch (ex: BackendException) {
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 @@ -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 @@ -90,7 +93,7 @@ internal class HttpClient(
return@withTimeout makeRequestIODispatcher(url, method, jsonBody, timeout, headers)
}
} catch (e: TimeoutCancellationException) {
Logging.error("HttpClient: Request timed out: $url", e)
Logging.info("HttpClient: Request timed out: $url", e)
return HttpResponse(0, null, e)
} catch (e: Throwable) {
return HttpResponse(0, null, e)
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 @@ -272,7 +272,7 @@ internal class OperationRepo(
ExecutionResult.FAIL_NORETRY,
ExecutionResult.FAIL_CONFLICT,
-> {
Logging.error("Operation execution failed without retry: $operations")
Logging.warn("Operation execution failed without retry: $operations")
// on failure we remove the operation from the store and wake any waiters
ops.forEach { _operationModelStore.remove(it.operation.id) }
ops.forEach { it.waiter?.wake(false) }
Expand All @@ -287,7 +287,7 @@ internal class OperationRepo(
}
}
ExecutionResult.FAIL_RETRY -> {
Logging.error("Operation execution failed, retrying: $operations")
Logging.info("Operation execution failed, retrying: $operations")
// add back all operations to the front of the queue to be re-executed.
synchronized(queue) {
ops.reversed().forEach {
Expand Down Expand Up @@ -349,7 +349,7 @@ internal class OperationRepo(
val delayForOnRetries = retries * _configModelStore.model.opRepoDefaultFailRetryBackoff
val delayFor = max(delayForOnRetries, retryAfterSecondsNonNull * 1_000)
if (delayFor < 1) return
Logging.error("Operations being delay for: $delayFor ms")
Logging.debug("Operations being delay for: $delayFor ms")
withTimeoutOrNull(delayFor) {
retryWaiter.waitForWake()
}
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()
}
Loading
Loading