diff --git a/bugsnag-android-core/src/main/java/com/bugsnag/android/AppDataCollector.kt b/bugsnag-android-core/src/main/java/com/bugsnag/android/AppDataCollector.kt index a5c37ae3c2..c1eb409bc8 100644 --- a/bugsnag-android-core/src/main/java/com/bugsnag/android/AppDataCollector.kt +++ b/bugsnag-android-core/src/main/java/com/bugsnag/android/AppDataCollector.kt @@ -64,7 +64,7 @@ internal class AppDataCollector( ) } - fun generateEmptyEventAppWithState(): AppWithState { + fun generateHistoricAppWithState(): AppWithState { return AppWithState( config, binaryArch, packageName, releaseStage, versionName, codeBundleId, null, null, null, null diff --git a/bugsnag-android-core/src/main/java/com/bugsnag/android/DeviceDataCollector.kt b/bugsnag-android-core/src/main/java/com/bugsnag/android/DeviceDataCollector.kt index b894e22bd5..b7e3deb253 100644 --- a/bugsnag-android-core/src/main/java/com/bugsnag/android/DeviceDataCollector.kt +++ b/bugsnag-android-core/src/main/java/com/bugsnag/android/DeviceDataCollector.kt @@ -104,7 +104,7 @@ internal class DeviceDataCollector( Date(now) ) - fun generateEmptyEventDeviceWithState(timeStamp: Long) = + fun generateHistoricDeviceWithState(timeStamp: Long) = DeviceWithState( buildInfo, checkIsRooted(), diff --git a/bugsnag-plugin-android-exitinfo/api/bugsnag-plugin-android-exitinfo.api b/bugsnag-plugin-android-exitinfo/api/bugsnag-plugin-android-exitinfo.api index 5a3a43aded..4cd427fcb0 100644 --- a/bugsnag-plugin-android-exitinfo/api/bugsnag-plugin-android-exitinfo.api +++ b/bugsnag-plugin-android-exitinfo/api/bugsnag-plugin-android-exitinfo.api @@ -1,4 +1,5 @@ public final class com/bugsnag/android/BugsnagExitInfoPlugin : com/bugsnag/android/Plugin { + public static final field Companion Lcom/bugsnag/android/BugsnagExitInfoPlugin$Companion; public fun ()V public fun (Lcom/bugsnag/android/ExitInfoPluginConfiguration;)V public synthetic fun (Lcom/bugsnag/android/ExitInfoPluginConfiguration;ILkotlin/jvm/internal/DefaultConstructorMarker;)V @@ -6,17 +7,24 @@ public final class com/bugsnag/android/BugsnagExitInfoPlugin : com/bugsnag/andro public fun unload ()V } +public final class com/bugsnag/android/BugsnagExitInfoPlugin$Companion { +} + public final class com/bugsnag/android/ExitInfoPluginConfiguration { public fun ()V - public fun (ZZZ)V - public synthetic fun (ZZZILkotlin/jvm/internal/DefaultConstructorMarker;)V + public fun (ZZZZZ)V + public synthetic fun (ZZZZZILkotlin/jvm/internal/DefaultConstructorMarker;)V public fun equals (Ljava/lang/Object;)Z public final fun getDisableProcessStateSummaryOverride ()Z public final fun getIncludeLogcat ()Z public final fun getListOpenFds ()Z + public final fun getReportUnmatchedAnrs ()Z + public final fun getReportUnmatchedNativeCrashes ()Z public fun hashCode ()I public final fun setDisableProcessStateSummaryOverride (Z)V public final fun setIncludeLogcat (Z)V public final fun setListOpenFds (Z)V + public final fun setReportUnmatchedAnrs (Z)V + public final fun setReportUnmatchedNativeCrashes (Z)V } diff --git a/bugsnag-plugin-android-exitinfo/src/main/java/com/bugsnag/android/BugsnagExitInfoPlugin.kt b/bugsnag-plugin-android-exitinfo/src/main/java/com/bugsnag/android/BugsnagExitInfoPlugin.kt index cd5a30eb57..08056c8997 100644 --- a/bugsnag-plugin-android-exitinfo/src/main/java/com/bugsnag/android/BugsnagExitInfoPlugin.kt +++ b/bugsnag-plugin-android-exitinfo/src/main/java/com/bugsnag/android/BugsnagExitInfoPlugin.kt @@ -38,6 +38,7 @@ class BugsnagExitInfoPlugin @JvmOverloads constructor( val exitInfoPluginStore = ExitInfoPluginStore(client.immutableConfig) + addAllExitInfoAtFirstRun(client, exitInfoPluginStore) val (oldPid, exitInfoKeys) = exitInfoPluginStore.load() exitInfoPluginStore.persist(android.os.Process.myPid(), exitInfoKeys) @@ -59,6 +60,25 @@ class BugsnagExitInfoPlugin @JvmOverloads constructor( client.addOnSend(exitInfoCallback) } + private fun addAllExitInfoAtFirstRun( + client: Client, + exitInfoPluginStore: ExitInfoPluginStore + ) { + if (exitInfoPluginStore.isFirstRun || exitInfoPluginStore.legacyStore) { + val am: ActivityManager = client.appContext.safeGetActivityManager() ?: return + val allExitInfo: List = + am.getHistoricalProcessExitReasons( + client.appContext.packageName, + MATCH_ALL, + MAX_EXIT_REASONS + ) + + allExitInfo.forEach { exitInfo -> + exitInfoPluginStore.addExitInfoKey(ExitInfoKey(exitInfo.pid, exitInfo.timestamp)) + } + } + } + private fun createExitInfoCallback( client: Client, oldPid: Int?, @@ -82,7 +102,9 @@ class BugsnagExitInfoPlugin @JvmOverloads constructor( val eventSynthesizer = EventSynthesizer( traceEventEnhancer, tombstoneEventEnhancer, - exitInfoPluginStore + exitInfoPluginStore, + configuration.reportUnmatchedAnrs, + configuration.reportUnmatchedNativeCrashes ) val context = client.appContext val am: ActivityManager = context.safeGetActivityManager() ?: return @@ -103,4 +125,9 @@ class BugsnagExitInfoPlugin @JvmOverloads constructor( } catch (e: Exception) { null } + + companion object { + private const val MATCH_ALL = 0 + private const val MAX_EXIT_REASONS = 100 + } } diff --git a/bugsnag-plugin-android-exitinfo/src/main/java/com/bugsnag/android/EventSynthesizer.kt b/bugsnag-plugin-android-exitinfo/src/main/java/com/bugsnag/android/EventSynthesizer.kt index 55e33b525f..d723e3cba4 100644 --- a/bugsnag-plugin-android-exitinfo/src/main/java/com/bugsnag/android/EventSynthesizer.kt +++ b/bugsnag-plugin-android-exitinfo/src/main/java/com/bugsnag/android/EventSynthesizer.kt @@ -13,6 +13,8 @@ import android.app.ActivityManager.RunningAppProcessInfo.IMPORTANCE_VISIBLE import android.app.ActivityManager.RunningAppProcessInfo.REASON_PROVIDER_IN_USE import android.app.ActivityManager.RunningAppProcessInfo.REASON_SERVICE_IN_USE import android.app.ApplicationExitInfo +import android.app.ApplicationExitInfo.REASON_ANR +import android.app.ApplicationExitInfo.REASON_CRASH_NATIVE import android.os.Build import androidx.annotation.RequiresApi @@ -20,48 +22,73 @@ import androidx.annotation.RequiresApi internal class EventSynthesizer( private val anrEventEnhancer: (Event, ApplicationExitInfo) -> Unit, private val nativeEnhancer: (Event, ApplicationExitInfo) -> Unit, - private val exitInfoPluginStore: ExitInfoPluginStore + private val exitInfoPluginStore: ExitInfoPluginStore, + private val reportUnmatchedAnrs: Boolean, + private val reportUnmatchedNativeCrashes: Boolean ) { fun createEventWithExitInfo(appExitInfo: ApplicationExitInfo): Event? { val (_, knownExitInfoKeys) = exitInfoPluginStore.load() val exitInfoKey = ExitInfoKey(appExitInfo) - if (knownExitInfoKeys.contains(exitInfoKey)) return null else exitInfoPluginStore.addExitInfoKey( - exitInfoKey - ) + if (knownExitInfoKeys.contains(exitInfoKey)) return null + else exitInfoPluginStore.addExitInfoKey(exitInfoKey) when (appExitInfo.reason) { - ApplicationExitInfo.REASON_ANR - -> { - val newAnrEvent = InternalHooks.createEmptyANR(exitInfoKey.timestamp) - addExitInfoMetadata(newAnrEvent, appExitInfo) - anrEventEnhancer(newAnrEvent, appExitInfo) - val thread = - newAnrEvent.threads.find { it.name == "main" } - ?: newAnrEvent.threads.firstOrNull() - val error = newAnrEvent.addError("ANR", appExitInfo.description) - thread?.let { error.stacktrace.addAll(it.stacktrace) } - - return newAnrEvent + REASON_ANR -> { + return createEventWithUnmatchedAnrsReport(exitInfoKey, appExitInfo) } - ApplicationExitInfo.REASON_CRASH_NATIVE - -> { - val newNativeEvent = InternalHooks.createEmptyCrash(exitInfoKey.timestamp) - addExitInfoMetadata(newNativeEvent, appExitInfo) - nativeEnhancer(newNativeEvent, appExitInfo) - val thread = - newNativeEvent.threads.find { it.name == "main" } - ?: newNativeEvent.threads.firstOrNull() - val error = newNativeEvent.addError("Native", appExitInfo.description) - thread?.let { error.stacktrace.addAll(it.stacktrace) } - return newNativeEvent + REASON_CRASH_NATIVE -> { + return createEventWithUnmatchedNativeCrashesReport(exitInfoKey, appExitInfo) } else -> return null } } + private fun createEventWithUnmatchedAnrsReport( + exitInfoKey: ExitInfoKey, + appExitInfo: ApplicationExitInfo + ): Event? { + if (reportUnmatchedAnrs) { + val newAnrEvent = InternalHooks.createEmptyANR(exitInfoKey.timestamp) + addExitInfoMetadata(newAnrEvent, appExitInfo) + anrEventEnhancer(newAnrEvent, appExitInfo) + val thread = getErrorThread(newAnrEvent) + val error = newAnrEvent.addError("ANR", appExitInfo.description) + thread?.let { error.stacktrace.addAll(it.stacktrace) } + + return newAnrEvent + } else { + return null + } + } + + private fun createEventWithUnmatchedNativeCrashesReport( + exitInfoKey: ExitInfoKey, + appExitInfo: ApplicationExitInfo + ): Event? { + if (reportUnmatchedNativeCrashes) { + val newNativeEvent = InternalHooks.createEmptyCrash(exitInfoKey.timestamp) + addExitInfoMetadata(newNativeEvent, appExitInfo) + nativeEnhancer(newNativeEvent, appExitInfo) + val thread = + getErrorThread(newNativeEvent) + val error = newNativeEvent.addError("Native", appExitInfo.description) + thread?.let { error.stacktrace.addAll(it.stacktrace) } + return newNativeEvent + } else { + return null + } + } + + private fun getErrorThread(newNativeEvent: Event): Thread? { + val thread = + newNativeEvent.threads.find { it.name == "main" } + ?: newNativeEvent.threads.firstOrNull() + return thread + } + private fun addExitInfoMetadata( newEvent: Event, appExitInfo: ApplicationExitInfo @@ -72,8 +99,12 @@ internal class EventSynthesizer( "importance", getExitInfoImportance(appExitInfo.importance) ) - newEvent.addMetadata("exitinfo", "pss", appExitInfo.pss) - newEvent.addMetadata("exitinfo", "rss", appExitInfo.rss) + newEvent.addMetadata( + "exitinfo", "Proportional Set Size (PSS)", "${appExitInfo.pss} kB" + ) + newEvent.addMetadata( + "exitinfo", "Resident Set Size (RSS)", "${appExitInfo.rss} kB" + ) } private fun getExitInfoImportance(importance: Int): String = when (importance) { diff --git a/bugsnag-plugin-android-exitinfo/src/main/java/com/bugsnag/android/ExitInfoCallback.kt b/bugsnag-plugin-android-exitinfo/src/main/java/com/bugsnag/android/ExitInfoCallback.kt index b704a6de2d..6c401f851d 100644 --- a/bugsnag-plugin-android-exitinfo/src/main/java/com/bugsnag/android/ExitInfoCallback.kt +++ b/bugsnag-plugin-android-exitinfo/src/main/java/com/bugsnag/android/ExitInfoCallback.kt @@ -49,8 +49,10 @@ internal class ExitInfoCallback( exitInfo.reason == ApplicationExitInfo.REASON_SIGNALED ) { nativeEnhancer(event, exitInfo) + exitInfoPluginStore?.addExitInfoKey(ExitInfoKey(exitInfo)) } else if (exitInfo.reason == ApplicationExitInfo.REASON_ANR) { anrEventEnhancer(event, exitInfo) + exitInfoPluginStore?.addExitInfoKey(ExitInfoKey(exitInfo)) } } catch (exc: Throwable) { return true diff --git a/bugsnag-plugin-android-exitinfo/src/main/java/com/bugsnag/android/ExitInfoPluginConfiguration.kt b/bugsnag-plugin-android-exitinfo/src/main/java/com/bugsnag/android/ExitInfoPluginConfiguration.kt index a9882bd84b..227ab90bfe 100644 --- a/bugsnag-plugin-android-exitinfo/src/main/java/com/bugsnag/android/ExitInfoPluginConfiguration.kt +++ b/bugsnag-plugin-android-exitinfo/src/main/java/com/bugsnag/android/ExitInfoPluginConfiguration.kt @@ -11,6 +11,21 @@ class ExitInfoPluginConfiguration( */ var includeLogcat: Boolean = false, + /** + * Report [ApplicationExitInfo] ANRs that do not appear to correspond with BugSnag [Event]s + * as synthesized errors. These will appear on your dashboard without BugSnag data such as + * breadcrumbs and metadata, but will report crashes that BugSnag is otherwise unable to catch + * such as background ANRs. + */ + var reportUnmatchedAnrs: Boolean = true, + + /** + * Report [ApplicationExitInfo] native crashes that do not appear to correspond with BugSnag [Event]s + * as synthesized errors. These will appear on your dashboard without BugSnag data such as + * breadcrumbs and metadata, but will report crashes that BugSnag is otherwise unable to catch. + */ + var reportUnmatchedNativeCrashes: Boolean = true, + /** * Turn off event correlation based on the * [processStateSummary](ActivityManager.setProcessStateSummary) field. This can set to `true` @@ -18,21 +33,37 @@ class ExitInfoPluginConfiguration( */ var disableProcessStateSummaryOverride: Boolean = false ) { - constructor() : this(true, false, false) + constructor() : this( + listOpenFds = true, + includeLogcat = false, + reportUnmatchedAnrs = true, + reportUnmatchedNativeCrashes = true, + disableProcessStateSummaryOverride = false + ) internal fun copy() = - ExitInfoPluginConfiguration(listOpenFds, includeLogcat, disableProcessStateSummaryOverride) + ExitInfoPluginConfiguration( + listOpenFds, + includeLogcat, + disableProcessStateSummaryOverride, + reportUnmatchedAnrs, + reportUnmatchedNativeCrashes + ) override fun equals(other: Any?): Boolean { return other is ExitInfoPluginConfiguration && listOpenFds == other.listOpenFds && includeLogcat == other.includeLogcat && + reportUnmatchedAnrs == other.reportUnmatchedAnrs && + reportUnmatchedNativeCrashes == other.reportUnmatchedNativeCrashes && disableProcessStateSummaryOverride == other.disableProcessStateSummaryOverride } override fun hashCode(): Int { var result = listOpenFds.hashCode() result = 31 * result + includeLogcat.hashCode() + result = 31 * result + reportUnmatchedAnrs.hashCode() + result = 31 * result + reportUnmatchedNativeCrashes.hashCode() result = 31 * result + disableProcessStateSummaryOverride.hashCode() return result } diff --git a/bugsnag-plugin-android-exitinfo/src/main/java/com/bugsnag/android/ExitInfoPluginStore.kt b/bugsnag-plugin-android-exitinfo/src/main/java/com/bugsnag/android/ExitInfoPluginStore.kt index 493ec38650..51759d19b5 100644 --- a/bugsnag-plugin-android-exitinfo/src/main/java/com/bugsnag/android/ExitInfoPluginStore.kt +++ b/bugsnag-plugin-android-exitinfo/src/main/java/com/bugsnag/android/ExitInfoPluginStore.kt @@ -10,7 +10,10 @@ internal class ExitInfoPluginStore(config: ImmutableConfig) { private val file: File = File(config.persistenceDirectory.value, "bugsnag-exit-reasons") private val logger: Logger = config.logger private val lock = ReentrantReadWriteLock() - private val isFirstRun: Boolean = !file.exists() + internal val isFirstRun: Boolean = !file.exists() + + internal var legacyStore: Boolean = false + private set fun persist(currentPid: Int, exitInfoKeys: Set) { lock.writeLock().withLock { @@ -31,10 +34,11 @@ internal class ExitInfoPluginStore(config: ImmutableConfig) { } fun load(): Pair> { - if (isFirstRun) { - return null to emptySet() + return if (isFirstRun) { + null to emptySet() + } else { + tryLoadJson() ?: (tryLoadLegacy() to emptySet()) } - return tryLoadJson() ?: (tryLoadLegacy() to emptySet()) } private fun tryLoadJson(): Pair>? { @@ -62,6 +66,7 @@ internal class ExitInfoPluginStore(config: ImmutableConfig) { private fun tryLoadLegacy(): Int? { try { val content = file.readText() + legacyStore = true if (content.isEmpty()) { logger.w("PID is empty") return null @@ -75,7 +80,7 @@ internal class ExitInfoPluginStore(config: ImmutableConfig) { fun addExitInfoKey(exitInfoKey: ExitInfoKey) { val (oldPid, exitInfoKeys) = load() - val newExitInfoKeys = exitInfoKeys.toMutableSet().plus(exitInfoKey) + val newExitInfoKeys = exitInfoKeys + exitInfoKey oldPid?.let { persist(it, newExitInfoKeys) } } } diff --git a/bugsnag-plugin-android-exitinfo/src/main/java/com/bugsnag/android/InternalHooks.java b/bugsnag-plugin-android-exitinfo/src/main/java/com/bugsnag/android/InternalHooks.java index 163ff6be52..f2ea073632 100644 --- a/bugsnag-plugin-android-exitinfo/src/main/java/com/bugsnag/android/InternalHooks.java +++ b/bugsnag-plugin-android-exitinfo/src/main/java/com/bugsnag/android/InternalHooks.java @@ -23,8 +23,8 @@ static void deliver(@NonNull Client client, @NonNull Event event) { static Event createEmptyANR(long exitInfoTimeStamp) { Event event = NativeInterface.createEmptyEvent(); event.setDevice(client.deviceDataCollector - .generateEmptyEventDeviceWithState(exitInfoTimeStamp)); - event.setApp(client.appDataCollector.generateEmptyEventAppWithState()); + .generateHistoricDeviceWithState(exitInfoTimeStamp)); + event.setApp(client.appDataCollector.generateHistoricAppWithState()); event.updateSeverityReason(SeverityReason.REASON_ANR); return event; } @@ -32,8 +32,8 @@ static Event createEmptyANR(long exitInfoTimeStamp) { static Event createEmptyCrash(long exitInfoTimeStamp) { Event event = NativeInterface.createEmptyEvent(); event.setDevice(client.deviceDataCollector - .generateEmptyEventDeviceWithState(exitInfoTimeStamp)); - event.setApp(client.appDataCollector.generateEmptyEventAppWithState()); + .generateHistoricDeviceWithState(exitInfoTimeStamp)); + event.setApp(client.appDataCollector.generateHistoricAppWithState()); event.updateSeverityReason(SeverityReason.REASON_SIGNAL); return event; } diff --git a/examples/sdk-app-example/app/src/main/java/com/example/bugsnag/android/ExampleApplication.kt b/examples/sdk-app-example/app/src/main/java/com/example/bugsnag/android/ExampleApplication.kt index 668ef475c0..e43650bba6 100644 --- a/examples/sdk-app-example/app/src/main/java/com/example/bugsnag/android/ExampleApplication.kt +++ b/examples/sdk-app-example/app/src/main/java/com/example/bugsnag/android/ExampleApplication.kt @@ -50,4 +50,4 @@ class ExampleApplication : Application() { performNativeBugsnagSetup() } -} +} \ No newline at end of file diff --git a/features/fixtures/mazerunner/jvm-scenarios/src/main/java/com/bugsnag/android/mazerunner/BugsnagConfig.kt b/features/fixtures/mazerunner/jvm-scenarios/src/main/java/com/bugsnag/android/mazerunner/BugsnagConfig.kt index 7a594a9f78..7a0b99ca37 100644 --- a/features/fixtures/mazerunner/jvm-scenarios/src/main/java/com/bugsnag/android/mazerunner/BugsnagConfig.kt +++ b/features/fixtures/mazerunner/jvm-scenarios/src/main/java/com/bugsnag/android/mazerunner/BugsnagConfig.kt @@ -9,6 +9,7 @@ import com.bugsnag.android.DeliveryParams import com.bugsnag.android.DeliveryStatus import com.bugsnag.android.EndpointConfiguration import com.bugsnag.android.EventPayload +import com.bugsnag.android.ExitInfoPluginConfiguration import com.bugsnag.android.Logger import com.bugsnag.android.Session import com.bugsnag.android.createDefaultDelivery @@ -22,7 +23,14 @@ fun prepareConfig( ): Configuration { val config = Configuration(apiKey) if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { - config.addPlugin(BugsnagExitInfoPlugin()) + config.addPlugin( + BugsnagExitInfoPlugin( + ExitInfoPluginConfiguration( + reportUnmatchedAnrs = false, + reportUnmatchedNativeCrashes = false + ) + ) + ) } if (notify.isNotEmpty() && sessions.isNotEmpty()) { diff --git a/features/smoke_tests/04_unhandled.feature b/features/smoke_tests/04_unhandled.feature index 6eb54cffb8..041c29d7e4 100644 --- a/features/smoke_tests/04_unhandled.feature +++ b/features/smoke_tests/04_unhandled.feature @@ -214,6 +214,16 @@ Feature: Unhandled smoke tests And the event "metaData.opaque.array.1" equals "b" And the event "metaData.opaque.array.2" equals "c" + + @skip_below_android_12 + Scenario: Signal raised with exit info + When I set the screen orientation to portrait + And I run "CXXSignalSmokeScenario" and relaunch the crashed app + And I configure Bugsnag for "CXXSignalSmokeScenario" + And I wait to receive an error + And the event "metaData.Open FileDescriptors" is not null + And the event "metaData.app.exitReason" equals "crash native" + @debug-safe Scenario: C++ exception thrown with overwritten config When I set the screen orientation to portrait diff --git a/features/support/env.rb b/features/support/env.rb index d2eff5d3a0..df8670cf39 100644 --- a/features/support/env.rb +++ b/features/support/env.rb @@ -40,6 +40,10 @@ skip_this_scenario("Skipping scenario") if Maze.config.os_version < 11 end +Before('@skip_below_android_12') do |scenario| + skip_this_scenario("Skipping scenario") if Maze.config.os_version < 12 +end + Before('@skip_below_android_9') do |scenario| skip_this_scenario("Skipping scenario") if Maze.config.os_version < 9 end