From e23af6283194e27f691fb4a6f6e9ef62e89f0feb Mon Sep 17 00:00:00 2001 From: YingYing Chen <40571804+YYChen01988@users.noreply.github.com> Date: Thu, 14 Nov 2024 17:50:51 +0000 Subject: [PATCH] synthesize native crash and anr reports (#2094) * feat(ExitInfo): ExitInfo setOnEventStoreEmptyCallback * feat(ExitInfo): ExitInfo setOnEventStoreEmptyCallback * feat(ExitInfo): Track exitinfokey with event * feat(ExitInfo):new event with Anr exitInfo * refactor(EventStore): reworked the EventStore callback to only be called once, and only when the store is completely empty (no files in queue or in storage) * feat(ExitInfo):new event with Anr exitInfo * feat(ExitInfo): ExitInfo setOnEventStoreEmptyCallback * refactor(EventStore): reworked the EventStore callback to only be called once, and only when the store is completely empty (no files in queue or in storage) * refactor(EventStore): reworked the EventStore callback to only be called once, and only when the store is completely empty (no files in queue or in storage) * feat(ExitInfo): ExitInfo setOnEventStoreEmptyCallback * feat(ExitInfo):new event with Anr exitInfo * feat(ExitInfo):register EventStoreEmptyCallback in exitinfo plugin * feat(ExitInfo)synthesize native crash reports * feat(ExitInfo)add exit infos at first run --------- Co-authored-by: jason --- .../com/bugsnag/android/AppDataCollector.kt | 7 + .../bugsnag/android/DeviceDataCollector.kt | 14 ++ .../api/bugsnag-plugin-android-exitinfo.api | 12 +- .../detekt-baseline.xml | 2 + .../bugsnag/android/BugsnagExitInfoPlugin.kt | 104 ++++++++++++-- .../com/bugsnag/android/EventSynthesizer.kt | 134 ++++++++++++++++++ .../com/bugsnag/android/ExitInfoCallback.kt | 2 + .../android/ExitInfoPluginConfiguration.kt | 35 ++++- .../bugsnag/android/ExitInfoPluginStore.kt | 6 +- .../com/bugsnag/android/InternalHooks.java | 30 +++- .../bugsnag/android/ExampleApplication.kt | 2 +- .../android/mazerunner/BugsnagConfig.kt | 10 +- features/smoke_tests/04_unhandled.feature | 10 ++ features/support/env.rb | 4 + 14 files changed, 350 insertions(+), 22 deletions(-) create mode 100644 bugsnag-plugin-android-exitinfo/src/main/java/com/bugsnag/android/EventSynthesizer.kt 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 a74579eea9..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,6 +64,13 @@ internal class AppDataCollector( ) } + fun generateHistoricAppWithState(): AppWithState { + return AppWithState( + config, binaryArch, packageName, releaseStage, versionName, codeBundleId, + null, null, null, null + ) + } + @SuppressLint("SwitchIntDef") @Suppress("DEPRECATION") private fun getProcessImportance(): String? { 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 493763a493..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,6 +104,20 @@ internal class DeviceDataCollector( Date(now) ) + fun generateHistoricDeviceWithState(timeStamp: Long) = + DeviceWithState( + buildInfo, + checkIsRooted(), + internalDeviceId, + locale, + null, + runtimeVersions.toMutableMap(), + null, + null, + getOrientationAsString(), + Date(timeStamp) + ) + fun getDeviceMetadata(): Map { val map = HashMap() populateBatteryInfo(into = map) 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/detekt-baseline.xml b/bugsnag-plugin-android-exitinfo/detekt-baseline.xml index 0409f89bcd..43db92ae23 100644 --- a/bugsnag-plugin-android-exitinfo/detekt-baseline.xml +++ b/bugsnag-plugin-android-exitinfo/detekt-baseline.xml @@ -2,9 +2,11 @@ + CyclomaticComplexMethod:EventSynthesizer.kt$EventSynthesizer$private fun getExitInfoImportance(importance: Int): String CyclomaticComplexMethod:ExitInfoCallback.kt$ExitInfoCallback$@SuppressLint("SwitchIntDef") @Suppress("DEPRECATION") private fun importanceDescriptionOf(exitInfo: ApplicationExitInfo) CyclomaticComplexMethod:ExitInfoCallback.kt$ExitInfoCallback$private fun exitReasonOf(exitInfo: ApplicationExitInfo) LongParameterList:TombstoneParser.kt$TombstoneParser$( exitInfo: ApplicationExitInfo, listOpenFds: Boolean, includeLogcat: Boolean, threadConsumer: (BugsnagThread) -> Unit, fileDescriptorConsumer: (Int, String, String) -> Unit, logcatConsumer: (String) -> Unit ) + MagicNumber:BugsnagExitInfoPlugin.kt$BugsnagExitInfoPlugin$100 MagicNumber:TraceParser.kt$TraceParser$16 MagicNumber:TraceParser.kt$TraceParser$3 MaxLineLength:ExitInfoCallback.kt$ExitInfoCallback$val allExitInfo: List<ApplicationExitInfo> = am.getHistoricalProcessExitReasons(context.packageName, 0, MAX_EXIT_INFO) 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 2427fadfca..a6f9c44249 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 @@ -1,6 +1,8 @@ package com.bugsnag.android +import android.annotation.SuppressLint import android.app.ActivityManager +import android.app.ApplicationExitInfo import android.content.Context import android.os.Build import androidx.annotation.RequiresApi @@ -12,6 +14,7 @@ class BugsnagExitInfoPlugin @JvmOverloads constructor( private val configuration = configuration.copy() + @SuppressLint("VisibleForTests") override fun load(client: Client) { if (!configuration.disableProcessStateSummaryOverride) { client.addOnSession( @@ -23,28 +26,96 @@ class BugsnagExitInfoPlugin @JvmOverloads constructor( ) } + val tombstoneEventEnhancer = TombstoneEventEnhancer( + client.logger, + configuration.listOpenFds, + configuration.includeLogcat + ) + val traceEventEnhancer = TraceEventEnhancer( + client.logger, + client.immutableConfig.projectPackages + ) + val exitInfoPluginStore = ExitInfoPluginStore(client.immutableConfig) + addAllExitInfoAtFirstRun(client, exitInfoPluginStore) exitInfoPluginStore.currentPid = android.os.Process.myPid() - val exitInfoCallback = ExitInfoCallback( - client.appContext, - exitInfoPluginStore.previousPid, - TombstoneEventEnhancer( - client.logger, - configuration.listOpenFds, - configuration.includeLogcat - ), - TraceEventEnhancer( - client.logger, - client.immutableConfig.projectPackages - ), - exitInfoPluginStore + val exitInfoCallback = createExitInfoCallback( + client, + exitInfoPluginStore.previousPid, exitInfoPluginStore, + tombstoneEventEnhancer, + traceEventEnhancer ) - + InternalHooks.setEventStoreEmptyCallback(client) { + synthesizeNewEvents( + client, + exitInfoPluginStore, + tombstoneEventEnhancer, + traceEventEnhancer + ) + } 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?, + exitInfoPluginStore: ExitInfoPluginStore, + tombstoneEventEnhancer: TombstoneEventEnhancer, + traceEventEnhancer: TraceEventEnhancer + ): ExitInfoCallback = ExitInfoCallback( + client.appContext, + oldPid, + tombstoneEventEnhancer, + traceEventEnhancer, + exitInfoPluginStore + ) + + private fun synthesizeNewEvents( + client: Client, + exitInfoPluginStore: ExitInfoPluginStore, + tombstoneEventEnhancer: TombstoneEventEnhancer, + traceEventEnhancer: TraceEventEnhancer + ) { + val eventSynthesizer = EventSynthesizer( + traceEventEnhancer, + tombstoneEventEnhancer, + exitInfoPluginStore, + configuration.reportUnmatchedAnrs, + configuration.reportUnmatchedNativeCrashes + ) + val context = client.appContext + val am: ActivityManager = context.safeGetActivityManager() ?: return + val allExitInfo: List = + am.getHistoricalProcessExitReasons(context.packageName, 0, 100) + allExitInfo.forEach { + val newEvent = eventSynthesizer.createEventWithExitInfo(it) + if (newEvent != null) { + InternalHooks.deliver(client, newEvent) + } + } + } + override fun unload() = Unit private fun Context.safeGetActivityManager(): ActivityManager? = try { @@ -52,4 +123,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 new file mode 100644 index 0000000000..f334dfb81a --- /dev/null +++ b/bugsnag-plugin-android-exitinfo/src/main/java/com/bugsnag/android/EventSynthesizer.kt @@ -0,0 +1,134 @@ +package com.bugsnag.android + +import android.app.ActivityManager.RunningAppProcessInfo.IMPORTANCE_CACHED +import android.app.ActivityManager.RunningAppProcessInfo.IMPORTANCE_CANT_SAVE_STATE +import android.app.ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND +import android.app.ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND_SERVICE +import android.app.ActivityManager.RunningAppProcessInfo.IMPORTANCE_GONE +import android.app.ActivityManager.RunningAppProcessInfo.IMPORTANCE_PERCEPTIBLE +import android.app.ActivityManager.RunningAppProcessInfo.IMPORTANCE_PERCEPTIBLE_PRE_26 +import android.app.ActivityManager.RunningAppProcessInfo.IMPORTANCE_SERVICE +import android.app.ActivityManager.RunningAppProcessInfo.IMPORTANCE_TOP_SLEEPING +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 + +@RequiresApi(Build.VERSION_CODES.R) +internal class EventSynthesizer( + private val anrEventEnhancer: (Event, ApplicationExitInfo) -> Unit, + private val nativeEnhancer: (Event, ApplicationExitInfo) -> Unit, + private val exitInfoPluginStore: ExitInfoPluginStore, + private val reportUnmatchedAnrs: Boolean, + private val reportUnmatchedNativeCrashes: Boolean +) { + fun createEventWithExitInfo(appExitInfo: ApplicationExitInfo): Event? { + val knownExitInfoKeys = exitInfoPluginStore.exitInfoKeys + val exitInfoKey = ExitInfoKey(appExitInfo) + + if (knownExitInfoKeys.contains(exitInfoKey)) return null + else exitInfoPluginStore.addExitInfoKey(exitInfoKey) + + when (appExitInfo.reason) { + REASON_ANR -> { + return createEventWithUnmatchedAnrsReport(exitInfoKey, appExitInfo) + } + + 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 + ) { + newEvent.addMetadata("exitinfo", "description", appExitInfo.description) + newEvent.addMetadata( + "exitinfo", + "importance", + getExitInfoImportance(appExitInfo.importance) + ) + 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) { + IMPORTANCE_FOREGROUND -> "foreground" + IMPORTANCE_FOREGROUND_SERVICE -> "foreground service" + IMPORTANCE_TOP_SLEEPING -> "top sleeping" + IMPORTANCE_TOP_SLEEPING_PRE_28 -> "top sleeping" + IMPORTANCE_VISIBLE -> "visible" + IMPORTANCE_PERCEPTIBLE -> "perceptible" + IMPORTANCE_PERCEPTIBLE_PRE_26 -> "perceptible" + IMPORTANCE_CANT_SAVE_STATE -> "can't save state" + IMPORTANCE_CANT_SAVE_STATE_PRE_26 -> "can't save state" + IMPORTANCE_SERVICE -> "service" + IMPORTANCE_CACHED -> "cached/background" + IMPORTANCE_GONE -> "gone" + IMPORTANCE_EMPTY -> "empty" + REASON_PROVIDER_IN_USE -> "provider in use" + REASON_SERVICE_IN_USE -> "service in use" + else -> "unknown importance ($importance)" + } + + companion object { + const val IMPORTANCE_EMPTY = 500 + const val IMPORTANCE_CANT_SAVE_STATE_PRE_26 = 170 + const val IMPORTANCE_TOP_SLEEPING_PRE_28 = 150 + } +} 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 8b74888792..b587df17fd 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,6 +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() + internal val isFirstRun: Boolean = !file.exists() + + internal var legacyStore: Boolean = false + private set var previousPid: Int = 0 private set @@ -43,7 +47,7 @@ internal class ExitInfoPluginStore(config: ImmutableConfig) { } } - fun persist() { + private fun persist() { lock.writeLock().withLock { try { file.writer().buffered().use { writer -> 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 85fd09ac13..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 @@ -1,13 +1,41 @@ package com.bugsnag.android; +import static com.bugsnag.android.Bugsnag.client; + +import androidx.annotation.NonNull; + import kotlin.Unit; import kotlin.jvm.functions.Function0; class InternalHooks { - private InternalHooks() {} + private InternalHooks() { + } public static void setEventStoreEmptyCallback(Client client, Function0 callback) { client.eventStore.setOnEventStoreEmptyCallback(callback); } + + static void deliver(@NonNull Client client, @NonNull Event event) { + client.deliveryDelegate.deliver(event); + } + + static Event createEmptyANR(long exitInfoTimeStamp) { + Event event = NativeInterface.createEmptyEvent(); + event.setDevice(client.deviceDataCollector + .generateHistoricDeviceWithState(exitInfoTimeStamp)); + event.setApp(client.appDataCollector.generateHistoricAppWithState()); + event.updateSeverityReason(SeverityReason.REASON_ANR); + return event; + } + + static Event createEmptyCrash(long exitInfoTimeStamp) { + Event event = NativeInterface.createEmptyEvent(); + event.setDevice(client.deviceDataCollector + .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