Skip to content

Commit

Permalink
Add Conditional Extraction of ApplicationExitInfo Trace Data for Enha…
Browse files Browse the repository at this point in the history
…nced Logging from getTraceInputStream
  • Loading branch information
jaredsburrows committed Nov 4, 2024
1 parent 1c77b4a commit d3bc58c
Show file tree
Hide file tree
Showing 2 changed files with 128 additions and 12 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -140,18 +140,38 @@ internal class AppExitLogger(

@TargetApi(Build.VERSION_CODES.R)
private fun ApplicationExitInfo.toFields(): InternalFieldsMap {
// https://developer.android.com/reference/kotlin/android/app/ApplicationExitInfo
return mapOf(
"_app_exit_source" to "ApplicationExitInfo",
"_app_exit_process_name" to this.processName,
"_app_exit_reason" to this.reason.toReasonText(),
"_app_exit_importance" to this.importance.toImportanceText(),
"_app_exit_status" to this.status.toString(),
"_app_exit_pss" to this.pss.toString(),
"_app_exit_rss" to this.rss.toString(),
"_app_exit_description" to this.description.orEmpty(),
// TODO(murki): Extract getTraceInputStream() for REASON_ANR or REASON_CRASH_NATIVE
).toFields()
return buildMap {
put("_app_exit_source", "ApplicationExitInfo")
put("_app_exit_process_name", processName)
put("_app_exit_reason", reason.toReasonText())
put("_app_exit_importance", importance.toImportanceText())
put("_app_exit_status", status.toString())
put("_app_exit_pss", pss.toString())
put("_app_exit_rss", rss.toString())
put("_app_exit_description", description.orEmpty())

// Add trace data if available for REASON_ANR or REASON_CRASH_NATIVE
if (reason == ApplicationExitInfo.REASON_ANR || reason == ApplicationExitInfo.REASON_CRASH_NATIVE) {
runCatching {
getTraceInputStream()?.takeIf { it.available() > 0 }?.bufferedReader()?.use { reader ->
reader.readText().takeIf { it.isNotEmpty() }
}
}.onSuccess { trace ->
trace?.let { put("_app_exit_trace", it) }
}.onFailure { e ->
// Log or handle the exception as needed
logger.log(
LogType.LIFECYCLE,
LogLevel.ERROR,
mapOf(
"_app_exit_source" to "ApplicationExitInfo",
"_app_exit_reason" to "TraceReadFailure",
"_app_exit_details" to e.message.orEmpty(),
).toFields(),
) { APP_EXIT_EVENT_NAME }
}
}
}.toFields()
}

private fun Int.toReasonText(): String {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import org.junit.Test
import org.mockito.ArgumentMatchers.anyInt
import org.mockito.Mockito
import org.mockito.Mockito.RETURNS_DEEP_STUBS
import java.io.ByteArrayInputStream
import java.io.IOException
import java.nio.charset.StandardCharsets

Expand Down Expand Up @@ -168,6 +169,101 @@ class AppExitLoggerTest {
)
}

@Test
fun testLogPreviousExitReasonIfAnyWithTrace() {
// ARRANGE
val sessionId = "test-session-id"
val timestamp = 123L
val traceData = "Test trace data"

val mockExitInfo = mock<ApplicationExitInfo>(defaultAnswer = RETURNS_DEEP_STUBS)
whenever(mockExitInfo.processStateSummary).thenReturn(sessionId.toByteArray(StandardCharsets.UTF_8))
whenever(mockExitInfo.timestamp).thenReturn(timestamp)
whenever(mockExitInfo.processName).thenReturn("test-process-name")
whenever(mockExitInfo.reason).thenReturn(ApplicationExitInfo.REASON_ANR)
whenever(mockExitInfo.importance).thenReturn(RunningAppProcessInfo.IMPORTANCE_FOREGROUND)
whenever(mockExitInfo.status).thenReturn(0)
whenever(mockExitInfo.pss).thenReturn(1)
whenever(mockExitInfo.rss).thenReturn(2)
whenever(mockExitInfo.description).thenReturn("test-description")
whenever(mockExitInfo.getTraceInputStream()).thenReturn(ByteArrayInputStream(traceData.toByteArray()))

whenever(activityManager.getHistoricalProcessExitReasons(anyOrNull(), any(), any())).thenReturn(listOf(mockExitInfo))

// ACT
appExitLogger.logPreviousExitReasonIfAny()

// ASSERT
val expectedFields = mapOf(
"_app_exit_source" to "ApplicationExitInfo",
"_app_exit_process_name" to "test-process-name",
"_app_exit_reason" to "ANR",
"_app_exit_importance" to "FOREGROUND",
"_app_exit_status" to "0",
"_app_exit_pss" to "1",
"_app_exit_rss" to "2",
"_app_exit_description" to "test-description",
"_app_exit_trace" to traceData,
).toFields()

verify(logger).log(
eq(LogType.LIFECYCLE),
eq(LogLevel.ERROR),
eq(expectedFields),
eq(null),
eq(LogAttributesOverrides(sessionId, timestamp)),
eq(false),
argThat { i: () -> String -> i.invoke() == "AppExit" },
)
}

@Test
fun testLogPreviousExitReasonIfAnyWithoutTrace() {
// ARRANGE
val sessionId = "test-session-id"
val timestamp = 123L

val mockExitInfo = mock<ApplicationExitInfo>(defaultAnswer = RETURNS_DEEP_STUBS)
whenever(mockExitInfo.processStateSummary).thenReturn(sessionId.toByteArray(StandardCharsets.UTF_8))
whenever(mockExitInfo.timestamp).thenReturn(timestamp)
whenever(mockExitInfo.processName).thenReturn("test-process-name")
whenever(mockExitInfo.reason).thenReturn(ApplicationExitInfo.REASON_ANR)
whenever(mockExitInfo.importance).thenReturn(RunningAppProcessInfo.IMPORTANCE_FOREGROUND)
whenever(mockExitInfo.status).thenReturn(0)
whenever(mockExitInfo.pss).thenReturn(1)
whenever(mockExitInfo.rss).thenReturn(2)
whenever(mockExitInfo.description).thenReturn("test-description")
whenever(mockExitInfo.getTraceInputStream()).thenReturn(null) // Simulate no trace data available

whenever(activityManager.getHistoricalProcessExitReasons(anyOrNull(), any(), any())).thenReturn(listOf(mockExitInfo))

// ACT
appExitLogger.logPreviousExitReasonIfAny()

// ASSERT
val expectedFields = mapOf(
"_app_exit_source" to "ApplicationExitInfo",
"_app_exit_process_name" to "test-process-name",
"_app_exit_reason" to "ANR",
"_app_exit_importance" to "FOREGROUND",
"_app_exit_status" to "0",
"_app_exit_pss" to "1",
"_app_exit_rss" to "2",
"_app_exit_description" to "test-description",
// _app_exit_trace is not present
).toFields()

verify(logger).log(
eq(LogType.LIFECYCLE),
eq(LogLevel.ERROR),
eq(expectedFields),
eq(null),
eq(LogAttributesOverrides(sessionId, timestamp)),
eq(false),
argThat { i: () -> String -> i.invoke() == "AppExit" },
)
}

@Test
fun testLogPreviousExitReasonIfAnyReportsError() {
// ARRANGE
Expand Down

0 comments on commit d3bc58c

Please sign in to comment.