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

synthesise anr reports #2116

Open
wants to merge 17 commits into
base: next
Choose a base branch
from
Open
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
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
# Changelog

## TBD

### Enhancements

* Introduced a new option in the `exitinfo` plugin for generating ANRs that does not have a matching Events (such as background ANRs)
[#2116](https://github.com/bugsnag/bugsnag-android/pull/2116)

## 6.10.0 (2024-11-14)

### Enhancements
Expand Down
1 change: 1 addition & 0 deletions bugsnag-android-core/api/bugsnag-android-core.api
Original file line number Diff line number Diff line change
Expand Up @@ -509,6 +509,7 @@ public class com/bugsnag/android/NativeInterface {
public static fun addMetadata (Ljava/lang/String;Ljava/lang/String;Ljava/lang/Object;)V
public static fun addMetadata (Ljava/lang/String;Ljava/util/Map;)V
public static fun clearMetadata (Ljava/lang/String;Ljava/lang/String;)V
public static fun createEmptyEvent ()Lcom/bugsnag/android/Event;
public static fun createEvent (Ljava/lang/Throwable;Lcom/bugsnag/android/Client;Lcom/bugsnag/android/SeverityReason;)Lcom/bugsnag/android/Event;
public static fun deliverReport (Ljava/io/File;)V
public static fun deliverReport ([B[B[BLjava/lang/String;Z)V
Expand Down
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 @@ -91,6 +91,20 @@ internal class DeviceDataCollector(
Date(now)
)

fun generateHistoricDeviceWithState(timeStamp: Long) =
DeviceWithState(
buildInfo,
checkIsRooted(),
deviceIdStore.get()?.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
Expand Up @@ -40,6 +40,10 @@ internal class EventStore(
private val callbackState: CallbackState
override val logger: Logger

var onEventStoreEmptyCallback: () -> Unit = {}
var onDiscardEventCallback: (EventPayload) -> Unit = {}
private var isEmptyEventCallbackCalled: Boolean = false

/**
* Flush startup crashes synchronously on the main thread. Startup crashes block the main thread
* when being sent (subject to [Configuration.setSendLaunchCrashesSynchronously])
Expand All @@ -51,7 +55,10 @@ internal class EventStore(
val future = try {
bgTaskService.submitTask(
TaskType.ERROR_REQUEST,
Runnable { flushLaunchCrashReport() }
Runnable {
flushLaunchCrashReport()
notifyEventQueueEmpty()
}
)
} catch (exc: RejectedExecutionException) {
logger.d("Failed to flush launch crash reports, continuing.", exc)
Expand Down Expand Up @@ -135,6 +142,7 @@ internal class EventStore(
logger.d("No regular events to flush to Bugsnag.")
}
flushReports(storedFiles)
notifyEventQueueEmpty()
}
)
} catch (exception: RejectedExecutionException) {
Expand Down Expand Up @@ -188,11 +196,13 @@ internal class EventStore(
logger.w(
"Discarding over-sized event (${eventFile.length()}) after failed delivery"
)
discardEvents(eventFile)
deleteStoredFiles(setOf(eventFile))
} else if (isTooOld(eventFile)) {
logger.w(
"Discarding historical event (from ${getCreationDate(eventFile)}) after failed delivery"
)
discardEvents(eventFile)
deleteStoredFiles(setOf(eventFile))
} else {
cancelQueuedFiles(setOf(eventFile))
Expand Down Expand Up @@ -258,6 +268,26 @@ internal class EventStore(
return Date(findTimestampInFilename(file))
}

private fun notifyEventQueueEmpty() {
if (isEmpty() && !isEmptyEventCallbackCalled) {
onEventStoreEmptyCallback()
isEmptyEventCallbackCalled = true
}
}

private fun discardEvents(eventFile: File) {
val eventFilenameInfo = fromFile(eventFile, config)
onDiscardEventCallback(
EventPayload(
eventFilenameInfo.apiKey,
null,
eventFile,
notifier,
config
)
)
}

companion object {
private const val LAUNCH_CRASH_TIMEOUT_MS: Long = 2000
val EVENT_COMPARATOR: Comparator<in File?> = Comparator { lhs, rhs ->
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,11 @@ internal abstract class FileStore(
return true
}

/**
* Test whether this `FileStore` is definitely empty
*/
fun isEmpty(): Boolean = queuedFiles.isEmpty() && storageDir.list().isNullOrEmpty()

fun enqueueContentForDelivery(content: String?, filename: String) {
if (!isStorageDirValid(storageDir)) {
return
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,8 @@ private static Client getClient() {
* will populate the Error and then pass the Event object to
* {@link Client#populateAndNotifyAndroidEvent(Event, OnErrorCallback)}.
*/
private static Event createEmptyEvent() {
@NonNull
public static Event createEmptyEvent() {
Client client = getClient();

return new Event(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
package com.bugsnag.android

import com.bugsnag.android.BugsnagTestUtils.generateConfiguration
import com.bugsnag.android.BugsnagTestUtils.generateEvent
import com.bugsnag.android.FileStore.Delegate
import com.bugsnag.android.internal.BackgroundTaskService
import com.bugsnag.android.internal.ImmutableConfig
import com.bugsnag.android.internal.convertToImmutableConfig
import org.junit.After
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
import org.junit.Before
import org.junit.Test
import org.mockito.ArgumentMatchers.any
import org.mockito.Mockito.mock
import org.mockito.Mockito.`when`
import java.io.File
import java.nio.file.Files
import java.util.concurrent.CountDownLatch

class EmptyEventCallbackTest {

private lateinit var storageDir: File
private lateinit var errorDir: File
private lateinit var backgroundTaskService: BackgroundTaskService

@Before
fun setUp() {
storageDir = Files.createTempDirectory("tmp").toFile()
storageDir.deleteRecursively()
errorDir = File(storageDir, "bugsnag/errors")
backgroundTaskService = BackgroundTaskService()
}

@After
fun tearDown() {
storageDir.deleteRecursively()
backgroundTaskService.shutdown()
}

@Test
fun emptyQueuedEventTriggerEventStoreEmptyCallback() {
val config = generateConfiguration().apply {
maxPersistedEvents = 0
persistenceDirectory = storageDir
}
val eventStore = createEventStore(convertToImmutableConfig(config))
eventStore.write(generateEvent())

val callbackLatch = CountDownLatch(1)
eventStore.onEventStoreEmptyCallback = { callbackLatch.countDown() }
eventStore.flushAsync()
callbackLatch.await()

assertTrue(eventStore.isEmpty())
}

@Test
fun testFailedDeliveryEvents() {
val mockDelivery = mock(Delivery::class.java)
`when`(mockDelivery.deliver(any<EventPayload>(), any<DeliveryParams>()))
.thenReturn(
DeliveryStatus.DELIVERED,
DeliveryStatus.FAILURE
)

val config = generateConfiguration().apply {
maxPersistedEvents = 3
persistenceDirectory = storageDir
delivery = mockDelivery
}
val eventStore = createEventStore(convertToImmutableConfig(config))
repeat(3) {
eventStore.write(generateEvent())
}

// the EventStore should not be considered empty with 3 events in it
assertFalse(eventStore.isEmpty())

var eventStoreEmptyCount = 0
eventStore.onEventStoreEmptyCallback = { eventStoreEmptyCount++ }
eventStore.flushAsync()
backgroundTaskService.shutdown()

assertTrue(
"there should be no undelivered payloads in the EventStore",
eventStore.isEmpty()
)

assertEquals(
"onEventStoreEmptyCallback have been called even with a failed (deleted) payload",
1,
eventStoreEmptyCount
)
}

@Test
fun testUndeliveredEvents() {
val mockDelivery = mock(Delivery::class.java)
`when`(mockDelivery.deliver(any<EventPayload>(), any<DeliveryParams>()))
.thenReturn(
DeliveryStatus.DELIVERED,
DeliveryStatus.FAILURE,
DeliveryStatus.UNDELIVERED
)

val config = generateConfiguration().apply {
maxPersistedEvents = 3
persistenceDirectory = storageDir
delivery = mockDelivery
}
val eventStore = createEventStore(convertToImmutableConfig(config))
repeat(3) {
eventStore.write(generateEvent())
}

// the EventStore should not be considered empty with 3 events in it
assertFalse(eventStore.isEmpty())

var eventStoreEmptyCount = 0
eventStore.onEventStoreEmptyCallback = { eventStoreEmptyCount++ }
eventStore.flushAsync()
backgroundTaskService.shutdown()

// the last payload should not have been delivered
assertFalse(
"there should be one undelivered payload in the EventStore",
eventStore.isEmpty()
)

assertEquals(
"onEventStoreEmptyCallback should not be called when there are undelivered payloads",
0,
eventStoreEmptyCount
)
}

private fun createEventStore(config: ImmutableConfig): EventStore {
return EventStore(
config,
NoopLogger,
Notifier(),
backgroundTaskService,
object : Delegate {
override fun onErrorIOFailure(
exception: Exception?,
errorFile: File?,
context: String?
) {
}
},
CallbackState()
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,17 @@ public final class com/bugsnag/android/BugsnagExitInfoPlugin : com/bugsnag/andro
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> (ZZZZ)V
public synthetic fun <init> (ZZZZILkotlin/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 getReportUnmatchedANR ()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 setReportUnmatchedANR (Z)V
}

7 changes: 5 additions & 2 deletions bugsnag-plugin-android-exitinfo/detekt-baseline.xml
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,13 @@
<SmellBaseline>
<ManuallySuppressedIssues/>
<CurrentIssues>
<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>CyclomaticComplexMethod:CodeStrings.kt$@RequiresApi(Build.VERSION_CODES.R) @SuppressLint("SwitchIntDef") @Suppress("DEPRECATION") internal fun importanceDescriptionOf(exitInfo: ApplicationExitInfo)</ID>
<ID>CyclomaticComplexMethod:CodeStrings.kt$@RequiresApi(Build.VERSION_CODES.R) internal 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:ExitInfoCallbackTest.kt$ExitInfoCallbackTest$exitInfoCallback = ExitInfoCallback(context, nativeEnhancer, anrEventEnhancer, null, ApplicationExitInfoMatcher(context, 100))</ID>
<ID>MaxLineLength:TraceParserInvalidStackframesTest.kt$TraceParserInvalidStackframesTest.Companion$"#01 pc 0000000000000c5c /data/app/~~sKQbJGqVJA5glcnvEjZCMg==/com.example.bugsnag.android-fVuoJh5GpAL7sRAeI3vjSw==/lib/arm64/libentrypoint.so "</ID>
<ID>MaxLineLength:TraceParserInvalidStackframesTest.kt$TraceParserInvalidStackframesTest.Companion$"#01 pc 0000000000000c5c /data/app/~~sKQbJGqVJA5glcnvEjZCMg==/com.example.bugsnag.android-fVuoJh5GpAL7sRAeI3vjSw==/lib/arm64/libentrypoint.so (Java_com_example_bugsnag_android_BaseCrashyActivity_anrFromCXX+20"</ID>
<ID>MaxLineLength:TraceParserInvalidStackframesTest.kt$TraceParserInvalidStackframesTest.Companion$"#01 pc 0000000000000c5c /data/app/~~sKQbJGqVJA5glcnvEjZCMg==/com.example.bugsnag.android-fVuoJh5GpAL7sRAeI3vjSw==/lib/arm64/libentrypoint.so (Java_com_example_bugsnag_android_BaseCrashyActivity_anrFromCXX+20) ("</ID>
Expand All @@ -28,5 +30,6 @@
<ID>ReturnCount:TraceParser.kt$TraceParser$@VisibleForTesting internal fun parseNativeFrame(line: String): Stackframe?</ID>
<ID>SwallowedException:BugsnagExitInfoPlugin.kt$BugsnagExitInfoPlugin$e: Exception</ID>
<ID>SwallowedException:ExitInfoCallback.kt$ExitInfoCallback$exc: Throwable</ID>
<ID>SwallowedException:ExitInfoPluginStore.kt$ExitInfoPluginStore$exc: Throwable</ID>
</CurrentIssues>
</SmellBaseline>
Loading
Loading