Skip to content

Commit

Permalink
synthesize native crash and anr reports (#2094)
Browse files Browse the repository at this point in the history
* 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 <[email protected]>
  • Loading branch information
YYChen01988 and lemnik authored Nov 14, 2024
1 parent 1c13c98 commit e23af62
Show file tree
Hide file tree
Showing 14 changed files with 350 additions and 22 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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? {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<String, Any?> {
val map = HashMap<String, Any?>()
populateBatteryInfo(into = map)
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
}

2 changes: 2 additions & 0 deletions bugsnag-plugin-android-exitinfo/detekt-baseline.xml
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,11 @@
<SmellBaseline>
<ManuallySuppressedIssues/>
<CurrentIssues>
<ID>CyclomaticComplexMethod:EventSynthesizer.kt$EventSynthesizer$private fun getExitInfoImportance(importance: Int): String</ID>
<ID>CyclomaticComplexMethod:ExitInfoCallback.kt$ExitInfoCallback$@SuppressLint("SwitchIntDef") @Suppress("DEPRECATION") private fun importanceDescriptionOf(exitInfo: ApplicationExitInfo)</ID>
<ID>CyclomaticComplexMethod:ExitInfoCallback.kt$ExitInfoCallback$private fun exitReasonOf(exitInfo: ApplicationExitInfo)</ID>
<ID>LongParameterList:TombstoneParser.kt$TombstoneParser$( exitInfo: ApplicationExitInfo, listOpenFds: Boolean, includeLogcat: Boolean, threadConsumer: (BugsnagThread) -> Unit, fileDescriptorConsumer: (Int, String, String) -> Unit, logcatConsumer: (String) -> Unit )</ID>
<ID>MagicNumber:BugsnagExitInfoPlugin.kt$BugsnagExitInfoPlugin$100</ID>
<ID>MagicNumber:TraceParser.kt$TraceParser$16</ID>
<ID>MagicNumber:TraceParser.kt$TraceParser$3</ID>
<ID>MaxLineLength:ExitInfoCallback.kt$ExitInfoCallback$val allExitInfo: List&lt;ApplicationExitInfo> = am.getHistoricalProcessExitReasons(context.packageName, 0, MAX_EXIT_INFO)</ID>
Expand Down
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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(
Expand All @@ -23,33 +26,106 @@ 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<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?,
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<ApplicationExitInfo> =
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 {
getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager
} 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
@@ -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
}
}
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
Loading

0 comments on commit e23af62

Please sign in to comment.