Skip to content

Commit

Permalink
feat(ExitInfo)add exit infos at first run
Browse files Browse the repository at this point in the history
  • Loading branch information
YYChen01988 committed Nov 12, 2024
1 parent afac6be commit b389819
Show file tree
Hide file tree
Showing 13 changed files with 163 additions and 47 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ internal class DeviceDataCollector(
Date(now)
)

fun generateEmptyEventDeviceWithState(timeStamp: Long) =
fun generateHistoricDeviceWithState(timeStamp: Long) =
DeviceWithState(
buildInfo,
checkIsRooted(),
Expand Down
Original file line number Diff line number Diff line change
@@ -1,22 +1,30 @@
public final class com/bugsnag/android/BugsnagExitInfoPlugin : com/bugsnag/android/Plugin {
public static final field Companion Lcom/bugsnag/android/BugsnagExitInfoPlugin$Companion;
public fun <init> ()V
public fun <init> (Lcom/bugsnag/android/ExitInfoPluginConfiguration;)V
public synthetic fun <init> (Lcom/bugsnag/android/ExitInfoPluginConfiguration;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
public fun load (Lcom/bugsnag/android/Client;)V
public fun unload ()V
}

public final class com/bugsnag/android/BugsnagExitInfoPlugin$Companion {
}

public final class com/bugsnag/android/ExitInfoPluginConfiguration {
public fun <init> ()V
public fun <init> (ZZZ)V
public synthetic fun <init> (ZZZILkotlin/jvm/internal/DefaultConstructorMarker;)V
public fun <init> (ZZZZZ)V
public synthetic fun <init> (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
}

Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand All @@ -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<ApplicationExitInfo> =
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?,
Expand All @@ -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
Expand All @@ -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
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,55 +13,82 @@ 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 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
Expand All @@ -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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,17 +11,38 @@ 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`
* to stop `BugsnagExitInfoPlugin` overwriting the field if it is being used by the app.
*/
var disableProcessStateSummaryOverride: Boolean = false
) {
constructor() : this(true, false, false)
constructor() : this(true, false, false, true, true)

internal fun copy() =
ExitInfoPluginConfiguration(listOpenFds, includeLogcat, disableProcessStateSummaryOverride)
ExitInfoPluginConfiguration(
listOpenFds,
includeLogcat,
disableProcessStateSummaryOverride,
reportUnmatchedAnrs,
reportUnmatchedNativeCrashes
)

override fun equals(other: Any?): Boolean {
return other is ExitInfoPluginConfiguration &&
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<ExitInfoKey>) {
lock.writeLock().withLock {
Expand All @@ -31,10 +34,11 @@ internal class ExitInfoPluginStore(config: ImmutableConfig) {
}

fun load(): Pair<Int?, Set<ExitInfoKey>> {
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<Int?, Set<ExitInfoKey>>? {
Expand Down Expand Up @@ -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
Expand All @@ -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) }
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,17 +23,17 @@ 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;
}

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;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,4 +50,4 @@ class ExampleApplication : Application() {
performNativeBugsnagSetup()
}

}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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()) {
Expand Down
10 changes: 10 additions & 0 deletions features/smoke_tests/04_unhandled.feature
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading

0 comments on commit b389819

Please sign in to comment.