Skip to content

Commit

Permalink
Extract data - if any - from getTraceInputStream
Browse files Browse the repository at this point in the history
  • Loading branch information
jaredsburrows committed Nov 4, 2024
1 parent 1c77b4a commit 2c21458
Show file tree
Hide file tree
Showing 2 changed files with 126 additions and 11 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -140,18 +140,37 @@ internal class AppExitLogger(

@TargetApi(Build.VERSION_CODES.R)
private fun ApplicationExitInfo.toFields(): InternalFieldsMap {
// https://developer.android.com/reference/kotlin/android/app/ApplicationExitInfo
return mapOf(
// Initialize the fields map with basic application exit information
val fields = mutableMapOf(
"_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()
"_app_exit_process_name" to processName,
"_app_exit_reason" to reason.toReasonText(),
"_app_exit_importance" to importance.toImportanceText(),
"_app_exit_status" to status.toString(),
"_app_exit_pss" to pss.toString(),
"_app_exit_rss" to rss.toString(),
"_app_exit_description" to description.orEmpty()
)

// Add trace data if available and the reason matches REASON_ANR or REASON_CRASH_NATIVE
if (reason == ApplicationExitInfo.REASON_ANR
|| reason == ApplicationExitInfo.REASON_CRASH_NATIVE) {
val traceData = getTraceInputStream()?.use { traceStream ->
// Check if there is any data in the stream
if (traceStream.available() > 0) {
traceStream.bufferedReader().use { it.readText() }
} else {
null
}
}

// Only add _app_exit_trace if traceData has content
if (!traceData.isNullOrEmpty()) {
fields["_app_exit_trace"] = traceData
}
}

return fields.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 2c21458

Please sign in to comment.