Skip to content

Commit 1211bd8

Browse files
AR Abdul Azeezcursoragent
andcommitted
feat: add logging_config-based remote logging control and SDK version gating
- Derive isRemoteLoggingEnabled from log_level presence in logging_config (empty object = disabled, has log_level = enabled) - Add OtelSdkSupport utility for testable SDK version checks (API 26+) - Gate all Otel initialization (crash, ANR, remote logging) on both SDK support and backend config - Rename OneSignalCrashLogInit to OneSignalOtelInit to reflect full scope - Simplify OneSignalCrashHandlerFactory with require() instead of no-op - Add crash test button in demo app SecondaryActivity - Fix Compose compiler plugin compatibility with AGP 8.8.2 - Add flow chart documentation for init sequence Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent 1ac7eb6 commit 1211bd8

21 files changed

Lines changed: 605 additions & 138 deletions

File tree

OneSignalSDK/onesignal/core/src/main/java/com/onesignal/core/internal/backend/IParamsBackendService.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,4 +58,5 @@ internal class FCMParamsObject(
5858

5959
internal class RemoteLoggingParamsObject(
6060
val logLevel: com.onesignal.debug.LogLevel? = null,
61+
val isEnabled: Boolean = logLevel != null && logLevel != com.onesignal.debug.LogLevel.NONE,
6162
)

OneSignalSDK/onesignal/core/src/main/java/com/onesignal/core/internal/config/ConfigModel.kt

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -454,4 +454,14 @@ class RemoteLoggingConfigModel(
454454
set(value) {
455455
setOptEnumProperty(::logLevel.name, value)
456456
}
457+
458+
/**
459+
* Whether remote logging is enabled.
460+
* Set by backend config hydration — true when the server sends a valid log_level, false otherwise.
461+
*/
462+
var isEnabled: Boolean
463+
get() = getBooleanProperty(::isEnabled.name) { false }
464+
set(value) {
465+
setBooleanProperty(::isEnabled.name, value)
466+
}
457467
}

OneSignalSDK/onesignal/core/src/main/java/com/onesignal/core/internal/config/impl/ConfigModelStoreListener.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,7 @@ internal class ConfigModelStoreListener(
104104
params.influenceParams.isUnattributedEnabled?.let { config.influenceParams.isUnattributedEnabled = it }
105105

106106
params.remoteLoggingParams.logLevel?.let { config.remoteLoggingParams.logLevel = it }
107+
config.remoteLoggingParams.isEnabled = params.remoteLoggingParams.isEnabled
107108

108109
_configModelStore.replace(config, ModelChangeTags.HYDRATE)
109110
success = true
Lines changed: 8 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,51 +1,38 @@
11
package com.onesignal.debug.internal.crash
22

33
import android.content.Context
4-
import android.os.Build
54
import com.onesignal.debug.internal.logging.Logging
65
import com.onesignal.debug.internal.logging.otel.android.createAndroidOtelPlatformProvider
76
import com.onesignal.otel.IOtelCrashHandler
87
import com.onesignal.otel.IOtelLogger
98
import com.onesignal.otel.OtelFactory
109

1110
/**
12-
* Factory for creating crash handlers with SDK version checks.
13-
* For SDK < 26, returns a no-op implementation.
14-
* For SDK >= 26, returns the Otel-based crash handler.
11+
* Factory for creating Otel-based crash handlers.
12+
* Callers must verify [OtelSdkSupport.isSupported] before calling [createCrashHandler].
1513
*
1614
* Uses minimal dependencies - only Context and logger.
1715
* Platform provider uses OtelIdResolver internally which reads from SharedPreferences.
1816
*/
1917
internal object OneSignalCrashHandlerFactory {
2018
/**
21-
* Creates a crash handler appropriate for the current SDK version.
22-
* This should be called as early as possible, before any other initialization.
19+
* Creates an Otel crash handler. Must only be called on supported devices
20+
* (SDK >= [OtelSdkSupport.MIN_SDK_VERSION]).
2321
*
2422
* @param context Android context for creating platform provider
2523
* @param logger Logger instance (can be shared with other components)
24+
* @throws IllegalArgumentException if called on an unsupported SDK
2625
*/
2726
fun createCrashHandler(
2827
context: Context,
2928
logger: IOtelLogger,
3029
): IOtelCrashHandler {
31-
// Otel requires SDK 26+, use no-op for older versions
32-
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
33-
Logging.info("OneSignal: Creating no-op crash handler (SDK ${Build.VERSION.SDK_INT} < 26)")
34-
return NoOpCrashHandler()
30+
require(OtelSdkSupport.isSupported) {
31+
"createCrashHandler called on unsupported SDK (< ${OtelSdkSupport.MIN_SDK_VERSION})"
3532
}
3633

37-
Logging.info("OneSignal: Creating Otel crash handler (SDK ${Build.VERSION.SDK_INT} >= 26)")
38-
// Create platform provider - uses OtelIdResolver internally
34+
Logging.info("OneSignal: Creating Otel crash handler (SDK >= ${OtelSdkSupport.MIN_SDK_VERSION})")
3935
val platformProvider = createAndroidOtelPlatformProvider(context)
4036
return OtelFactory.createCrashHandler(platformProvider, logger)
4137
}
4238
}
43-
44-
/**
45-
* No-op crash handler for SDK < 26.
46-
*/
47-
private class NoOpCrashHandler : IOtelCrashHandler {
48-
override fun initialize() {
49-
Logging.info("OneSignal: No-op crash handler initialized (SDK < 26, Otel not supported)")
50-
}
51-
}

OneSignalSDK/onesignal/core/src/main/java/com/onesignal/debug/internal/crash/OtelAnrDetector.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ import java.util.concurrent.atomic.AtomicLong
2323
* It creates its own crash reporter to save ANR reports.
2424
*/
2525
internal class OtelAnrDetector(
26-
private val openTelemetryCrash: IOtelOpenTelemetryCrash,
26+
openTelemetryCrash: IOtelOpenTelemetryCrash,
2727
private val logger: IOtelLogger,
2828
private val anrThresholdMs: Long = AnrConstants.DEFAULT_ANR_THRESHOLD_MS,
2929
private val checkIntervalMs: Long = AnrConstants.DEFAULT_CHECK_INTERVAL_MS,
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
package com.onesignal.debug.internal.crash
2+
3+
import android.os.Build
4+
5+
/**
6+
* Centralizes the SDK version requirement for Otel-based features
7+
* (crash reporting, ANR detection, remote log shipping).
8+
*
9+
* [isSupported] is writable internally so that unit tests can override
10+
* the device-level gate without Robolectric @Config gymnastics.
11+
*/
12+
internal object OtelSdkSupport {
13+
/** Otel libraries require Android O (API 26) or above. */
14+
const val MIN_SDK_VERSION = Build.VERSION_CODES.O // 26
15+
16+
/**
17+
* Whether the current device meets the minimum SDK requirement.
18+
* Production code should treat this as read-only; tests may flip it via [reset]/direct set.
19+
*/
20+
var isSupported: Boolean = Build.VERSION.SDK_INT >= MIN_SDK_VERSION
21+
internal set
22+
23+
/** Restores the runtime-detected value — call in test teardown. */
24+
fun reset() {
25+
isSupported = Build.VERSION.SDK_INT >= MIN_SDK_VERSION
26+
}
27+
}

OneSignalSDK/onesignal/core/src/main/java/com/onesignal/debug/internal/logging/otel/android/OtelIdResolver.kt

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -190,6 +190,23 @@ internal class OtelIdResolver(
190190
}
191191
}
192192

193+
/**
194+
* Resolves whether remote logging is enabled from cached ConfigModelStore.
195+
* Enabled is derived from the presence of a valid logLevel:
196+
* - "logging_config": {} → no logLevel → disabled (not on allowlist)
197+
* - "logging_config": {"log_level": "ERROR"} → has logLevel → enabled (on allowlist)
198+
* Returns false if not found, empty, or on error (disabled by default on first launch).
199+
*/
200+
@Suppress("TooGenericExceptionCaught", "SwallowedException")
201+
fun resolveRemoteLoggingEnabled(): Boolean {
202+
return try {
203+
val logLevel = resolveRemoteLogLevel()
204+
logLevel != null && logLevel != com.onesignal.debug.LogLevel.NONE
205+
} catch (e: Exception) {
206+
false
207+
}
208+
}
209+
193210
/**
194211
* Resolves remote log level from cached ConfigModelStore.
195212
* Returns null if not found or if there's an error.

OneSignalSDK/onesignal/core/src/main/java/com/onesignal/debug/internal/logging/otel/android/OtelPlatformProvider.kt

Lines changed: 10 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ internal class OtelPlatformProvider(
3434
override val appVersion: String = config.appVersion
3535
private val context: Context? = config.context
3636
private val getIsInForeground: (() -> Boolean?)? = config.getIsInForeground
37-
private val idResolver = OtelIdResolver(context)
37+
private val idResolver = OneSignalIdResolver(context)
3838

3939
// Top-level attributes (static, calculated once)
4040
override suspend fun getInstallId(): String = idResolver.resolveInstallId()
@@ -121,28 +121,23 @@ internal class OtelPlatformProvider(
121121
override val minFileAgeForReadMillis: Long = 5_000
122122

123123
// Remote logging configuration
124+
override val isRemoteLoggingEnabled: Boolean by lazy {
125+
idResolver.resolveRemoteLoggingEnabled()
126+
}
127+
124128
/**
125129
* The minimum log level to send remotely as a string.
126-
* - If remote config log level is populated and valid: use that level
127-
* - If remote config is null or unavailable: default to "ERROR" (always log errors)
128-
* - If remote config is explicitly NONE: return "NONE" (no logs including errors)
130+
* - "logging_config": {"log_level": "ERROR"} → returns "ERROR" (enabled, on allowlist)
131+
* - "logging_config": {} → returns null (disabled, not on allowlist)
132+
* - No config cached yet → returns null (first launch)
129133
*/
130134
@Suppress("TooGenericExceptionCaught", "SwallowedException")
131135
override val remoteLogLevel: String? by lazy {
132136
try {
133137
val configLevel = idResolver.resolveRemoteLogLevel()
134-
when {
135-
// Remote config is populated and working well - use whatever is sent from there
136-
configLevel != null && configLevel != com.onesignal.debug.LogLevel.NONE -> configLevel.name
137-
// Explicitly NONE means no logging (including errors)
138-
configLevel == com.onesignal.debug.LogLevel.NONE -> "NONE"
139-
// Remote config not available - default to ERROR (always log errors)
140-
else -> "ERROR"
141-
}
138+
configLevel?.name
142139
} catch (e: Exception) {
143-
// If there's an error accessing config, default to ERROR (always log errors)
144-
// Exception is intentionally swallowed to avoid recursion in logging
145-
"ERROR"
140+
null
146141
}
147142
}
148143

OneSignalSDK/onesignal/core/src/main/java/com/onesignal/internal/OneSignalImp.kt

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -207,20 +207,20 @@ internal class OneSignalImp(
207207
}
208208

209209
private fun initEssentials(context: Context) {
210-
// Create OneSignalCrashLogInit instance once - it manages platform provider lifecycle
211-
// Platform provider is created lazily and reused for both crash handler and logging
212-
val crashLogInit = OneSignalCrashLogInit(context)
210+
// Create OneSignalOtelInit instance once - it manages platform provider lifecycle
211+
// Platform provider is created lazily and reused for crash handler, ANR detector, and logging
212+
val otelInit = OneSignalOtelInit(context)
213213

214214
// Crash handler needs to be one of the first things we setup,
215215
// otherwise we'll not report some crashes, resulting in a false sense
216216
// of stability.
217217
// Initialize crash handler early, before any other services that might crash.
218218
// This is decoupled from getService to ensure fast initialization.
219-
crashLogInit.initializeCrashHandler()
219+
otelInit.initializeCrashHandler()
220220

221221
// Initialize Otel logging integration - reuses the same platform provider created in initializeCrashHandler
222222
// No service dependencies required, reads directly from SharedPreferences
223-
crashLogInit.initializeOtelLogging()
223+
otelInit.initializeOtelLogging()
224224

225225
PreferenceStoreFix.ensureNoObfuscatedPrefStore(context)
226226

OneSignalSDK/onesignal/core/src/main/java/com/onesignal/internal/OneSignalCrashLogInit.kt renamed to OneSignalSDK/onesignal/core/src/main/java/com/onesignal/internal/OneSignalOtelInit.kt

Lines changed: 25 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ package com.onesignal.internal
33
import android.content.Context
44
import com.onesignal.common.threading.suspendifyOnIO
55
import com.onesignal.debug.LogLevel
6+
import com.onesignal.debug.internal.crash.OneSignalCrashHandlerFactory
7+
import com.onesignal.debug.internal.crash.OtelSdkSupport
68
import com.onesignal.debug.internal.crash.createAnrDetector
79
import com.onesignal.debug.internal.logging.Logging
810
import com.onesignal.debug.internal.logging.otel.android.AndroidOtelLogger
@@ -14,12 +16,12 @@ import com.onesignal.otel.OtelFactory
1416
import com.onesignal.otel.crash.IOtelAnrDetector
1517

1618
/**
17-
* Helper class for OneSignal initialization tasks.
18-
* Extracted from OneSignalImp to reduce class size and improve maintainability.
19+
* Initializes all Otel-based observability features: crash reporting, ANR detection,
20+
* and remote log shipping.
1921
*
20-
* Creates and reuses a single OtelPlatformProvider instance for both crash handler and logging.
22+
* Creates and reuses a single OtelPlatformProvider instance across all features.
2123
*/
22-
internal class OneSignalCrashLogInit(
24+
internal class OneSignalOtelInit(
2325
private val context: Context,
2426
) {
2527
// Platform provider - created once and reused for both crash handler and logging
@@ -29,21 +31,27 @@ internal class OneSignalCrashLogInit(
2931

3032
@Suppress("TooGenericExceptionCaught")
3133
fun initializeCrashHandler() {
34+
if (!OtelSdkSupport.isSupported) {
35+
Logging.info("OneSignal: Device SDK < ${OtelSdkSupport.MIN_SDK_VERSION}, Otel not supported — skipping crash handler and ANR detector")
36+
return
37+
}
38+
if (!platformProvider.isRemoteLoggingEnabled) {
39+
Logging.info("OneSignal: Remote logging disabled (not yet enabled via config), skipping crash handler and ANR detector")
40+
return
41+
}
42+
3243
try {
3344
Logging.info("OneSignal: Initializing crash handler early...")
34-
Logging.info("OneSignal: Creating crash handler with minimal dependencies...")
3545

36-
// Create crash handler directly (non-blocking, doesn't require services upfront)
46+
// Use factory which handles SDK version checks (no-op for SDK < 26)
3747
val logger = AndroidOtelLogger()
38-
val crashHandler: IOtelCrashHandler = OtelFactory.createCrashHandler(platformProvider, logger)
48+
val crashHandler: IOtelCrashHandler = OneSignalCrashHandlerFactory.createCrashHandler(context, logger)
3949

4050
Logging.info("OneSignal: Crash handler created, initializing...")
4151
crashHandler.initialize()
4252

43-
// Log crash storage location for debugging
4453
Logging.info("OneSignal: ✅ Crash handler initialized successfully and ready to capture crashes")
4554
Logging.info("OneSignal: 📁 Crash logs will be stored at: ${platformProvider.crashStoragePath}")
46-
Logging.info("OneSignal: 💡 To view crash logs, use: adb shell run-as ${platformProvider.appPackageId} ls -la ${platformProvider.crashStoragePath}")
4755

4856
// Initialize ANR detector (standalone, monitors main thread for ANRs)
4957
try {
@@ -57,17 +65,23 @@ internal class OneSignalCrashLogInit(
5765
anrDetector.start()
5866
Logging.info("OneSignal: ✅ ANR detector initialized and started")
5967
} catch (e: Exception) {
60-
// If ANR detector initialization fails, log it but don't crash
6168
Logging.error("OneSignal: Failed to initialize ANR detector: ${e.message}", e)
6269
}
6370
} catch (e: Exception) {
64-
// If crash handler initialization fails, log it but don't crash
6571
Logging.error("OneSignal: Failed to initialize crash handler: ${e.message}", e)
6672
}
6773
}
6874

6975
@Suppress("TooGenericExceptionCaught")
7076
fun initializeOtelLogging() {
77+
if (!OtelSdkSupport.isSupported) {
78+
Logging.info("OneSignal: Device SDK < ${OtelSdkSupport.MIN_SDK_VERSION}, Otel not supported — skipping Otel logging")
79+
return
80+
}
81+
if (!platformProvider.isRemoteLoggingEnabled) {
82+
Logging.info("OneSignal: Remote logging disabled, skipping Otel logging integration")
83+
return
84+
}
7185
// Initialize Otel logging asynchronously to avoid blocking initialization
7286
// Remote logging is not critical for crashes, so it's safe to do this in the background
7387
// Uses OtelIdResolver internally which reads directly from SharedPreferences

0 commit comments

Comments
 (0)