Skip to content
Open
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 @@ -42,6 +42,7 @@ import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch

class MainApplicationKT : MultiDexApplication() {
Expand Down Expand Up @@ -81,6 +82,9 @@ class MainApplicationKT : MultiDexApplication() {
OneSignal.Notifications.requestPermission(true)

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

delay(3000)
//throw RuntimeException("test crash 2025-11-04 18")
}
}

Expand Down
2 changes: 1 addition & 1 deletion OneSignalSDK/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ buildscript {
huaweiAgconnectVersion = '1.9.1.304'
huaweiHMSPushVersion = '6.3.0.304'
huaweiHMSLocationVersion = '4.0.0.300'
kotlinVersion = '1.9.25'
kotlinVersion = '2.2.21'
coroutinesVersion = '1.7.3'
kotestVersion = '5.8.0'
ioMockVersion = '1.13.2'
Expand Down
12 changes: 12 additions & 0 deletions OneSignalSDK/onesignal/core/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,18 @@ dependencies {
}
}

implementation platform("io.opentelemetry:opentelemetry-bom:1.55.0")

implementation('io.opentelemetry:opentelemetry-api')
implementation('io.opentelemetry:opentelemetry-sdk')

implementation('io.opentelemetry:opentelemetry-exporter-otlp') // includes okhttp
implementation('io.opentelemetry.semconv:opentelemetry-semconv:1.37.0')
implementation('io.opentelemetry.contrib:opentelemetry-disk-buffering:1.51.0-alpha')
// TODO: Make sure by adding Android OTel that it doesn't auto send stuff to us



testImplementation(project(':OneSignal:testhelpers'))

testImplementation("io.kotest:kotest-runner-junit5:$kotestVersion")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,19 @@ 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.IOneSignalCrashReporter
import com.onesignal.debug.internal.crash.OneSignalCrashHandler
import com.onesignal.debug.internal.logging.otel.crash.IOneSignalCrashConfigProvider
import com.onesignal.debug.internal.logging.otel.IOneSignalOpenTelemetry
import com.onesignal.debug.internal.logging.otel.IOneSignalOpenTelemetryCrash
import com.onesignal.debug.internal.logging.otel.IOneSignalOpenTelemetryRemote
import com.onesignal.debug.internal.logging.otel.crash.OneSignalCrashConfigProvider
import com.onesignal.debug.internal.logging.otel.crash.OneSignalCrashReporterOtel
import com.onesignal.debug.internal.logging.otel.crash.OneSignalCrashUploader
import com.onesignal.debug.internal.logging.otel.OneSignalOpenTelemetryCrashLocal
import com.onesignal.debug.internal.logging.otel.OneSignalOpenTelemetryRemote
import com.onesignal.debug.internal.logging.otel.attributes.OneSignalOtelFieldsPerEvent
import com.onesignal.debug.internal.logging.otel.attributes.OneSignalOtelFieldsTopLevel
import com.onesignal.inAppMessages.IInAppMessagesManager
import com.onesignal.inAppMessages.internal.MisconfiguredIAMManager
import com.onesignal.location.ILocationManager
Expand Down Expand Up @@ -81,6 +94,20 @@ internal class CoreModule : IModule {
// Purchase Tracking
builder.register<TrackGooglePurchase>().provides<IStartableService>()

// Remote Crash and error logging
builder.register<OneSignalOpenTelemetryRemote>().provides<IOneSignalOpenTelemetry>()
builder.register<OneSignalCrashReporterOtel>().provides<IOneSignalCrashReporter>()
builder.register<OneSignalOpenTelemetryRemote>().provides<IOneSignalOpenTelemetryRemote>()

builder.register<OneSignalOpenTelemetryCrashLocal>().provides<IOneSignalOpenTelemetryCrash>()
builder.register<OneSignalCrashConfigProvider>().provides<IOneSignalCrashConfigProvider>()

builder.register<OneSignalCrashHandler>().provides<IStartableService>()
builder.register<OneSignalCrashUploader>().provides<IStartableService>()

builder.register<OneSignalOtelFieldsTopLevel>().provides<OneSignalOtelFieldsTopLevel>()
builder.register<OneSignalOtelFieldsPerEvent>().provides<OneSignalOtelFieldsPerEvent>()

// 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 @@ -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,5 @@
package com.onesignal.debug.internal.crash

internal interface IOneSignalCrashReporter {
suspend fun saveCrash(thread: Thread, throwable: Throwable)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
package com.onesignal.debug.internal.crash

import com.onesignal.core.internal.startup.IStartableService
import kotlinx.coroutines.runBlocking

/**
* Purpose: Writes any crashes involving OneSignal to a file where they can
* later be send to OneSignal to help improve reliability.
* NOTE: For future refactors, code is written assuming this is a singleton
*/
internal class OneSignalCrashHandler(
private val _crashReporter: IOneSignalCrashReporter,
) : IStartableService,
Thread.UncaughtExceptionHandler {
private var existingHandler: Thread.UncaughtExceptionHandler? = null
private val seenThrowables: MutableList<Throwable> = mutableListOf()

override fun start() {
existingHandler = Thread.getDefaultUncaughtExceptionHandler()
Thread.setDefaultUncaughtExceptionHandler(this)
}

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.
synchronized(seenThrowables) {
if (seenThrowables.contains(throwable)) {
return
}
seenThrowables.add(throwable)
}

// TODO: Catch anything we may throw and print only to logcat
// TODO: Also send a stop command to OneSignalCrashUploader,
// give a bit of time to finish and then call existingHandler.
// * This way the app doesn't have to open a 2nd time to get the
// crash report and should help prevent duplicated reports.
if (!isOneSignalAtFault(throwable)) {
existingHandler?.uncaughtException(thread, throwable)
return
}

/**
* NOTE: The order and running sequentially is important as:
* The existingHandler.uncaughtException can immediately terminate the
* process, either directly (if this is Android's
* KillApplicationHandler) OR the app's handler / 3rd party SDK (either
* directly or more likely, by it calling Android's
* KillApplicationHandler).
* Given this, we can't parallelize the existingHandler work with ours.
* The safest thing is to try to finish our work as fast as possible
* (including ensuring our logging write buffers are flushed) then call
* the existingHandler so any crash handlers the app also has gets the
* crash even too.
*
* NOTE: addShutdownHook() isn't a workaround as it doesn't fire for
* Process.killProcess, which KillApplicationHandler calls.
*/
runBlocking { _crashReporter.saveCrash(thread, throwable) }
existingHandler?.uncaughtException(thread, throwable)
}
}

internal fun isOneSignalAtFault(throwable: Throwable): Boolean =
throwable.stackTrace.any { it.className.startsWith("com.onesignal") }
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package com.onesignal.debug.internal.logging.otel

import io.opentelemetry.api.logs.LogRecordBuilder
import io.opentelemetry.sdk.common.CompletableResultCode
import io.opentelemetry.sdk.logs.export.LogRecordExporter

internal interface IOneSignalOpenTelemetry {
suspend fun getLogger(): LogRecordBuilder

suspend fun forceFlush(): CompletableResultCode
}

internal interface IOneSignalOpenTelemetryCrash : IOneSignalOpenTelemetry

internal interface IOneSignalOpenTelemetryRemote : IOneSignalOpenTelemetry {
val logExporter: LogRecordExporter
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
package com.onesignal.debug.internal.logging.otel

import android.os.Build
import androidx.annotation.RequiresApi
import com.onesignal.core.internal.config.ConfigModelStore
import com.onesignal.core.internal.http.impl.HTTP_SDK_VERSION_HEADER_KEY
import com.onesignal.core.internal.http.impl.HTTP_SDK_VERSION_HEADER_VALUE
import com.onesignal.debug.internal.logging.otel.attributes.OneSignalOtelFieldsPerEvent
import com.onesignal.debug.internal.logging.otel.attributes.OneSignalOtelFieldsTopLevel
import com.onesignal.debug.internal.logging.otel.config.OtelConfigCrashFile
import com.onesignal.debug.internal.logging.otel.config.OtelConfigRemoteOneSignal
import com.onesignal.debug.internal.logging.otel.config.OtelConfigShared
import com.onesignal.debug.internal.logging.otel.crash.IOneSignalCrashConfigProvider
import io.opentelemetry.api.logs.LogRecordBuilder
import io.opentelemetry.sdk.OpenTelemetrySdk
import io.opentelemetry.sdk.common.CompletableResultCode
import java.util.concurrent.TimeUnit
import kotlin.coroutines.resume
import kotlin.coroutines.suspendCoroutine

internal fun LogRecordBuilder.setAllAttributes(attributes: Map<String, String>): LogRecordBuilder {
attributes.forEach { this.setAttribute(it.key, it.value) }
return this
}

internal abstract class OneSignalOpenTelemetryBase(
private val _osTopLevelFields: OneSignalOtelFieldsTopLevel,
private val _osPerEventFields: OneSignalOtelFieldsPerEvent,
) : IOneSignalOpenTelemetry {
private val lock = Any()
private var sdkCachedValue: OpenTelemetrySdk? = null

protected suspend fun getSdk(): OpenTelemetrySdk {
val attributes = _osTopLevelFields.getAttributes()
synchronized(lock) {
var localSdk = sdkCachedValue
if (localSdk != null) {
return localSdk
}

localSdk = getSdkInstance(attributes)
sdkCachedValue = localSdk
return localSdk
}
}

protected abstract fun getSdkInstance(attributes: Map<String, String>): OpenTelemetrySdk

override suspend fun forceFlush(): CompletableResultCode {
val sdkLoggerProvider = getSdk().sdkLoggerProvider
return suspendCoroutine {
it.resume(
sdkLoggerProvider.forceFlush().join(10, TimeUnit.SECONDS)
)
}
}

override suspend fun getLogger(): LogRecordBuilder =
getSdk()
.sdkLoggerProvider
.loggerBuilder("loggerBuilder")
.build()
.logRecordBuilder()
.setAllAttributes(_osPerEventFields.getAttributes())
}

@RequiresApi(Build.VERSION_CODES.O)
internal class OneSignalOpenTelemetryRemote(
private val _configModelStore: ConfigModelStore,
_osTopLevelFields: OneSignalOtelFieldsTopLevel,
_osPerEventFields: OneSignalOtelFieldsPerEvent,
) : OneSignalOpenTelemetryBase(_osTopLevelFields, _osPerEventFields),
IOneSignalOpenTelemetryRemote {
val extraHttpHeaders by lazy {
mapOf(
"X-OneSignal-App-Id" to _configModelStore.model.appId,
HTTP_SDK_VERSION_HEADER_KEY to HTTP_SDK_VERSION_HEADER_VALUE,
"x-honeycomb-team" to "", // TODO: REMOVE
)
}

override val logExporter by lazy {
OtelConfigRemoteOneSignal.HttpRecordBatchExporter.create(extraHttpHeaders)
}

override fun getSdkInstance(attributes: Map<String, String>): OpenTelemetrySdk =
OpenTelemetrySdk
.builder()
.setLoggerProvider(
OtelConfigRemoteOneSignal.SdkLoggerProviderConfig.create(
OtelConfigShared.ResourceConfig.create(attributes),
extraHttpHeaders
)
).build()
}

internal class OneSignalOpenTelemetryCrashLocal(
private val _crashPathProvider: IOneSignalCrashConfigProvider,
_osTopLevelFields: OneSignalOtelFieldsTopLevel,
_osPerEventFields: OneSignalOtelFieldsPerEvent,
) : OneSignalOpenTelemetryBase(_osTopLevelFields, _osPerEventFields),
IOneSignalOpenTelemetryCrash {
override fun getSdkInstance(attributes: Map<String, String>): OpenTelemetrySdk =
OpenTelemetrySdk
.builder()
.setLoggerProvider(
OtelConfigCrashFile.SdkLoggerProviderConfig.create(
OtelConfigShared.ResourceConfig.create(
attributes
),
_crashPathProvider.path,
_crashPathProvider.minFileAgeForReadMillis,
)
).build()
}
Loading
Loading