Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

synthesize native crash and anr reports #2094

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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)
YYChen01988 marked this conversation as resolved.
Show resolved Hide resolved
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)
YYChen01988 marked this conversation as resolved.
Show resolved Hide resolved

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