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
29 changes: 29 additions & 0 deletions MIGRATION_GUIDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,35 @@ The above statement will bring in the entire OneSignalSDK and is the desired sta
- `com.onesignal:location`: Include to bring in location-based functionality.


## OpenTelemetry Dependency Removal

As of the release that removes the `:otel` module, the SDK no longer depends on OpenTelemetry. The `com.onesignal:otel` artifact is no longer published, and with it goes the entire `io.opentelemetry` dependency tree (`opentelemetry-api`, `-sdk`, `-exporter-otlp`, `-semconv`, and `opentelemetry-disk-buffering`). SDK diagnostics are now handled by the multiplatform `logger` module bundled inside `com.onesignal:core`, which has no third-party telemetry dependencies.

No public, supported API changed. The removal does delete internal API surface in `com.onesignal.debug.internal.logging` — most visibly `Logging.setOtelTelemetry`. That method took a parameter type (`IOtelOpenTelemetryRemote`) that only existed inside the `com.onesignal:otel` artifact, so no application could have compiled against it without depending on that artifact directly. If you did, remove the reference and rebuild.

For most integrations no action is required, but note the following:

- **If you declared `com.onesignal:otel` directly**, remove it. The artifact is no longer published.
- **If you added ProGuard/R8 rules for OneSignal's OpenTelemetry usage**, you can remove them. Rules such as the following are no longer needed, because those classes are never on the classpath via OneSignal:

```pro
-dontwarn com.fasterxml.jackson.core.**
-dontwarn com.google.auto.value.**
-dontwarn io.opentelemetry.api.incubator.**
-dontwarn io.opentelemetry.api.internal.**
```

- **If your app uses OpenTelemetry itself**, you no longer need to reconcile its version with OneSignal's. Whatever version you depend on is now the only one in your build, which removes a class of R8 "Missing class" failures caused by version skew between the two.
- **If you were excluding OpenTelemetry from the OneSignal dependency**, that exclusion is now a no-op and can be deleted.

One upgrade-time note about crash reports still buffered on disk when the upgrade happens. Which ones survive depends on the format they were written in:

- Reports written by the OpenTelemetry path use its disk-buffering format, which the new implementation cannot read. These are deleted unread on a subsequent launch, so a crash captured shortly before the upgrade may never arrive.
- Reports written by the newer logger path are already in the format the upgraded SDK uses, and are uploaded normally.

Reports captured from the upgraded version onward are unaffected.


## Code Modularization

The OneSignal SDK has been updated to be more modular in nature. The SDK has been split into namespaces and functionality previously in the static `OneSignal` class has been moved to the appropriate namespace. Some namespaces are only available if you include the associated module in your build (for simplicity, including module `com.onesignal:OneSignal` will automatically bring in all modules). The namespaces, their containing modules, and how to access them in code are as follows:
Expand Down
4 changes: 0 additions & 4 deletions OneSignalSDK/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,6 @@ 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 Down
13 changes: 13 additions & 0 deletions OneSignalSDK/coverage/jacoco.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,19 @@ subprojects {
testCoverageEnabled = true
}
}

// Robolectric loads classes through its own instrumenting classloader, which
// strips the source-location metadata JaCoCo uses to attribute execution. Without
// this, every class exercised only by a @RobolectricTest reports 0% coverage even
// when it is thoroughly tested.
testOptions {
unitTests.all {
jacoco {
includeNoLocationClasses = true
excludes = ['jdk.internal.*']
}
}
}
}

def coverageExcludes = [
Expand Down
4 changes: 1 addition & 3 deletions OneSignalSDK/onesignal/core/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -93,9 +93,7 @@ dependencies {
}
}

// Otel module dependency
implementation(project(':OneSignal:otel'))
// Shared KMP module (OpenTelemetry-free logger) — the eventual replacement for :otel.
// Shared KMP module — backs the SDK's observability pipeline (remote logging, crash, ANR).
implementation(project(':OneSignal:kmp'))
testImplementation(project(':OneSignal:testhelpers'))

Expand Down
6 changes: 3 additions & 3 deletions OneSignalSDK/onesignal/core/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">

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

<!-- 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 @@ -221,7 +221,7 @@ object OneSignalDispatchers {
*
* Background:
* The lazy `by lazy` properties below construct `ThreadPoolExecutor` instances and wrap them
* in `asCoroutineDispatcher() + SupervisorJob() + CoroutineScope(...)`. Production OTel
* in `asCoroutineDispatcher() + SupervisorJob() + CoroutineScope(...)`. Production remote-logging
* shows that when the **first** caller of [launchOnIO] / [launchOnSerialIO] is on the main
* thread (Activity-lifecycle handler, `JobService.onStartJob`, etc.), the construction cost
* — which includes a `kotlinx.coroutines.BuildersKt.launch` that hits
Expand Down Expand Up @@ -323,7 +323,7 @@ object OneSignalDispatchers {
* thread parked in a non-cancellable JVM wait (e.g. a `CountDownLatch.await()` that never gets
* released because the test asserted/failed first); a plain coroutine cancellation cannot free
* such a thread, so it permanently starves the small pool. Later specs that use the real pool
* (e.g. HttpClientTests' `launchOnIO {…}.join()`, OperationRepo, OtelIdResolver) then see
* (e.g. HttpClientTests' `launchOnIO {…}.join()`, OperationRepo, LoggerIdResolver) then see
* `launchOnIO` rejected/cancelled and observe null results.
*
* This atomically swaps in a fresh [Pools] generation and tears the old one down —
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
package com.onesignal.core.internal.http

/** Central API base URL used by all SDK HTTP traffic, including Otel log export. */
/** Central API base URL used by all SDK HTTP traffic, including remote 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/"
Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,13 @@
package com.onesignal.debug.internal.crash

import com.onesignal.logger.CrashData
import java.util.concurrent.atomic.AtomicLong

/**
* Pure, Android-free decision core for the ANR watchdog.
*
* All timing/classification/deduplication state lives here so it can be exercised deterministically
* on the JVM (with an injected clock) without a `Handler`, `Looper`, or real background thread. The
* Android shell ([OtelAnrDetector]) owns the thread, the real sleep, and the reporting side effects,
* and delegates every per-iteration decision to [evaluate].
* Pure, Android-free decision core for the ANR watchdog. All timing, classification and dedup
* state lives here; the Android shell
* ([com.onesignal.debug.internal.logging.logger.android.AndroidLogAnrDetector]) owns the thread,
* the real sleep and the reporting side effects, and delegates each iteration to [evaluate].
*
* Foreground and background blocks keep independent dedup timestamps: a stream of backgrounded
* warnings must never suppress a genuine foreground ANR (and vice versa).
Expand All @@ -21,7 +20,7 @@ internal class AnrCheckEvaluator(
private val dedupWindowMs: Long,
private val now: () -> Long,
) {
// Monotonic timestamps (from `now`); see OtelAnrDetector for why the clock must be monotonic.
// Monotonic timestamps (from `now`); see AndroidLogAnrDetector for why the clock must be monotonic.
private val lastResponseTime = AtomicLong(now())
private val lastForegroundReportTime = AtomicLong(NEVER_REPORTED)
private val lastBackgroundReportTime = AtomicLong(NEVER_REPORTED)
Expand Down Expand Up @@ -81,9 +80,8 @@ internal class AnrCheckEvaluator(
val nowMs = now()
val lastReport = lastReportHolder.get()

// Skip only if we actually reported this class of block recently. NEVER_REPORTED means we have
// not reported yet (or the main thread recovered), so we must not dedup — important shortly
// after boot when the monotonic clock is still small and `now - 0` would look "recent".
// NEVER_REPORTED must not dedup: shortly after boot the monotonic clock is still small and
// `now - 0` would look "recent", suppressing the very first block.
if (lastReport != NEVER_REPORTED && nowMs - lastReport <= dedupWindowMs) {
return AnrCheckResult.Deduped(durationMs = durationMs, sinceLastReportMs = nowMs - lastReport, inForeground = inForeground)
}
Expand Down Expand Up @@ -125,21 +123,11 @@ internal sealed interface AnrCheckResult {
data class BackgroundWarning(val durationMs: Long) : AnrCheckResult
}

/**
* How a watchdog check is interpreted. Kept separate from side effects so the decision is a pure,
* deterministically testable function of the measured timings and app state.
*/
/** How a watchdog check is interpreted; see [AnrCheckResult] for what each case means. */
internal enum class BlockClassification {
/** Main thread responded within the applicable threshold. */
RESPONSIVE,

/** The watchdog thread's own sleep overran — the process was frozen, not the main thread. */
FROZEN_PROCESS,

/** Foreground block beyond the ANR threshold: a real, user-visible ANR. */
FOREGROUND_ANR,

/** Background block beyond the background threshold: not an ANR, recorded as a warning. */
BACKGROUND_WARNING,
}

Expand Down Expand Up @@ -172,11 +160,77 @@ internal fun classifyBlock(

/**
* Compact fingerprint for a captured main-thread stack: the top frame plus the first OneSignal frame.
* Kept as a queryable summary so background blocks can be grouped/triaged without parsing the full
* stack. Pure (operates only on the array) so it is covered by plain JVM tests.
* Kept as a queryable summary so background blocks can be grouped and triaged without parsing the
* full stack.
*/
internal fun buildBlockFingerprint(stackTrace: Array<StackTraceElement>): String {
val topFrame = stackTrace.firstOrNull()?.toString() ?: "unknown"
val oneSignalFrame = stackTrace.firstOrNull { it.className.startsWith("com.onesignal") }?.toString() ?: "none"
return "top=$topFrame|onesignal=$oneSignalFrame"
}

/** Exception type for a foreground, user-visible ANR. Dashboards key off this exact value. */
internal const val ANR_EXCEPTION_TYPE = "ApplicationNotRespondingException"

/** Exception type for a backgrounded main-thread block, which is never an ANR. */
internal const val BACKGROUND_BLOCK_EXCEPTION_TYPE = "BackgroundMainThreadBlockException"

/**
* Renders a live thread's stack in the canonical JVM layout that [Throwable.stackTraceToString]
* emits: a `type: message` header followed by `\tat `-prefixed frames. ANR records are captured
* from a running thread rather than a thrown exception, so there is no throwable to serialize, but
* the output must stay byte-identical to the crash path's or consumers that parse
* `exception.stacktrace` stop matching ANRs only.
*/
internal fun formatJvmStacktrace(
exceptionType: String,
exceptionMessage: String,
stackTrace: Array<StackTraceElement>,
): String {
// Matches printStackTrace, which terminates every line with the platform separator.
val lineSeparator = System.lineSeparator()
return buildString {
append(exceptionType)
if (exceptionMessage.isNotEmpty()) {
append(": ").append(exceptionMessage)
}
append(lineSeparator)
for (frame in stackTrace) {
append("\tat ").append(frame).append(lineSeparator)
}
}
}

/** Builds the fatal ANR record for a foreground block of [unresponsiveDurationMs]. */
internal fun buildAnrCrashData(
threadName: String,
stackTrace: Array<StackTraceElement>,
unresponsiveDurationMs: Long,
): CrashData {
val message = "Application Not Responding: Main thread blocked for ${unresponsiveDurationMs}ms"
return CrashData(
threadName = threadName,
exceptionType = ANR_EXCEPTION_TYPE,
exceptionMessage = message,
stacktrace = formatJvmStacktrace(ANR_EXCEPTION_TYPE, message, stackTrace),
)
}

/**
* Builds the non-fatal record for a backgrounded main-thread block, with a
* [buildBlockFingerprint] summary embedded in the message.
*/
internal fun buildBackgroundBlockCrashData(
threadName: String,
stackTrace: Array<StackTraceElement>,
unresponsiveDurationMs: Long,
): CrashData {
val message =
"Background main-thread block for ${unresponsiveDurationMs}ms | ${buildBlockFingerprint(stackTrace)}"
return CrashData(
threadName = threadName,
exceptionType = BACKGROUND_BLOCK_EXCEPTION_TYPE,
exceptionMessage = message,
stacktrace = formatJvmStacktrace(BACKGROUND_BLOCK_EXCEPTION_TYPE, message, stackTrace),
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,14 @@ package com.onesignal.debug.internal.crash
import android.os.Build

/**
* Centralizes the SDK version requirement for Otel-based features
* Centralizes the SDK version requirement for observability 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. */
internal object ObservabilitySdkSupport {
/** The shared logger module requires Android O (API 26) or above. */
const val MIN_SDK_VERSION = Build.VERSION_CODES.O // 26

/**
Expand Down

This file was deleted.

Loading