diff --git a/bugsnag-android-core/api/bugsnag-android-core.api b/bugsnag-android-core/api/bugsnag-android-core.api
index 3236422146..43980ffb3c 100644
--- a/bugsnag-android-core/api/bugsnag-android-core.api
+++ b/bugsnag-android-core/api/bugsnag-android-core.api
@@ -275,6 +275,16 @@ public final class com/bugsnag/android/DeliveryStatus$Companion {
public final fun forHttpResponseCode (I)Lcom/bugsnag/android/DeliveryStatus;
}
+public final class com/bugsnag/android/DeliveryStrategy : java/lang/Enum {
+ public static final field SEND_IMMEDIATELY Lcom/bugsnag/android/DeliveryStrategy;
+ public static final field STORE_AND_FLUSH Lcom/bugsnag/android/DeliveryStrategy;
+ public static final field STORE_AND_SEND Lcom/bugsnag/android/DeliveryStrategy;
+ public static final field STORE_ONLY Lcom/bugsnag/android/DeliveryStrategy;
+ public static fun getEntries ()Lkotlin/enums/EnumEntries;
+ public static fun valueOf (Ljava/lang/String;)Lcom/bugsnag/android/DeliveryStrategy;
+ public static fun values ()[Lcom/bugsnag/android/DeliveryStrategy;
+}
+
public class com/bugsnag/android/Device : com/bugsnag/android/JsonStream$Streamable {
public final fun getCpuAbi ()[Ljava/lang/String;
public final fun getId ()Ljava/lang/String;
@@ -392,6 +402,7 @@ public class com/bugsnag/android/Event : com/bugsnag/android/FeatureFlagAware, c
public fun getApp ()Lcom/bugsnag/android/AppWithState;
public fun getBreadcrumbs ()Ljava/util/List;
public fun getContext ()Ljava/lang/String;
+ public fun getDeliveryStrategy ()Lcom/bugsnag/android/DeliveryStrategy;
public fun getDevice ()Lcom/bugsnag/android/DeviceWithState;
public fun getErrors ()Ljava/util/List;
public fun getFeatureFlags ()Ljava/util/List;
@@ -408,6 +419,7 @@ public class com/bugsnag/android/Event : com/bugsnag/android/FeatureFlagAware, c
public fun leaveBreadcrumb (Ljava/lang/String;Lcom/bugsnag/android/BreadcrumbType;Ljava/util/Map;)Lcom/bugsnag/android/Breadcrumb;
public fun setApiKey (Ljava/lang/String;)V
public fun setContext (Ljava/lang/String;)V
+ public fun setDeliveryStrategy (Lcom/bugsnag/android/DeliveryStrategy;)V
public fun setErrorReportingThread (J)V
public fun setErrorReportingThread (Lcom/bugsnag/android/Thread;)V
public fun setGroupingDiscriminator (Ljava/lang/String;)Ljava/lang/String;
diff --git a/bugsnag-android-core/detekt-baseline.xml b/bugsnag-android-core/detekt-baseline.xml
index c40c467a66..2e745383b8 100644
--- a/bugsnag-android-core/detekt-baseline.xml
+++ b/bugsnag-android-core/detekt-baseline.xml
@@ -20,7 +20,7 @@
LongParameterList:DeviceDataCollector.kt$DeviceDataCollector$( private val connectivity: Connectivity, private val appContext: Context, resources: Resources, private val deviceIdStore: Provider<DeviceIdStore.DeviceIds?>, private val buildInfo: DeviceBuildInfo, private val dataDirectory: File, private val rootedFuture: Provider<Boolean>?, private val bgTaskService: BackgroundTaskService, private val logger: Logger )
LongParameterList:DeviceWithState.kt$DeviceWithState$( buildInfo: DeviceBuildInfo, jailbroken: Boolean?, id: String?, locale: String?, totalMemory: Long?, runtimeVersions: MutableMap<String, Any>, /** * The number of free bytes of storage available on the device */ var freeDisk: Long?, /** * The number of free bytes of memory available on the device */ var freeMemory: Long?, /** * The orientation of the device when the event occurred: either portrait or landscape */ var orientation: String?, /** * The timestamp on the device when the event occurred */ var time: Date? )
LongParameterList:EventFilenameInfo.kt$EventFilenameInfo.Companion$( obj: Any, uuid: String = UUID.randomUUID().toString(), apiKey: String?, timestamp: Long = System.currentTimeMillis(), config: ImmutableConfig, isLaunching: Boolean? = null )
- LongParameterList:EventInternal.kt$EventInternal$( apiKey: String, logger: Logger, breadcrumbs: MutableList<Breadcrumb> = mutableListOf(), discardClasses: Set<Pattern> = setOf(), errors: MutableList<Error> = mutableListOf(), metadata: Metadata = Metadata(), featureFlags: FeatureFlags = FeatureFlags(), originalError: Throwable? = null, projectPackages: Collection<String> = setOf(), severityReason: SeverityReason = SeverityReason.newInstance(SeverityReason.REASON_HANDLED_EXCEPTION), threads: MutableList<Thread> = mutableListOf(), user: User = User(), redactionKeys: Set<Pattern>? = null )
+ LongParameterList:EventInternal.kt$EventInternal$( apiKey: String, logger: Logger, breadcrumbs: MutableList<Breadcrumb> = mutableListOf(), discardClasses: Set<Pattern> = setOf(), errors: MutableList<Error> = mutableListOf(), metadata: Metadata = Metadata(), featureFlags: FeatureFlags = FeatureFlags(), originalError: Throwable? = null, projectPackages: Collection<String> = setOf(), severityReason: SeverityReason = SeverityReason.newInstance(SeverityReason.REASON_HANDLED_EXCEPTION), threads: MutableList<Thread> = mutableListOf(), user: User = User(), redactionKeys: Set<Pattern>? = null, isAttemptDeliveryOnCrash: Boolean = false )
LongParameterList:EventStorageModule.kt$EventStorageModule$( contextModule: ContextModule, configModule: ConfigModule, dataCollectionModule: DataCollectionModule, bgTaskService: BackgroundTaskService, trackerModule: TrackerModule, systemServiceModule: SystemServiceModule, notifier: Notifier, callbackState: CallbackState )
LongParameterList:NativeStackframe.kt$NativeStackframe$( /** * The name of the method that was being executed */ var method: String?, /** * The location of the source file */ var file: String?, /** * The line number within the source file this stackframe refers to */ var lineNumber: Number?, /** * The address of the instruction where the event occurred. */ var frameAddress: Long?, /** * The address of the function where the event occurred. */ var symbolAddress: Long?, /** * The address of the library where the event occurred. */ var loadAddress: Long?, /** * Whether this frame identifies the program counter */ var isPC: Boolean?, /** * The type of the error */ var type: ErrorType? = null, /** * Identifies the exact build this frame originates from. */ var codeIdentifier: String? = null, )
LongParameterList:StateEvent.kt$StateEvent.Install$( @JvmField val apiKey: String, @JvmField val autoDetectNdkCrashes: Boolean, @JvmField val appVersion: String?, @JvmField val buildUuid: String?, @JvmField val releaseStage: String?, @JvmField val lastRunInfoPath: String, @JvmField val consecutiveLaunchCrashes: Int, @JvmField val sendThreads: ThreadSendPolicy, @JvmField val maxBreadcrumbs: Int )
diff --git a/bugsnag-android-core/src/main/java/com/bugsnag/android/DeliveryDelegate.java b/bugsnag-android-core/src/main/java/com/bugsnag/android/DeliveryDelegate.java
index cd0d9ddf1a..267265130b 100644
--- a/bugsnag-android-core/src/main/java/com/bugsnag/android/DeliveryDelegate.java
+++ b/bugsnag-android-core/src/main/java/com/bugsnag/android/DeliveryDelegate.java
@@ -1,7 +1,5 @@
package com.bugsnag.android;
-import static com.bugsnag.android.SeverityReason.REASON_PROMISE_REJECTION;
-
import com.bugsnag.android.internal.BackgroundTaskService;
import com.bugsnag.android.internal.ImmutableConfig;
import com.bugsnag.android.internal.TaskType;
@@ -54,23 +52,25 @@ void deliver(@NonNull Event event) {
}
}
- if (event.getImpl().getOriginalUnhandled()) {
- // should only send unhandled errors if they don't terminate the process (i.e. ANRs)
- String severityReasonType = event.getImpl().getSeverityReasonType();
- boolean promiseRejection = REASON_PROMISE_REJECTION.equals(severityReasonType);
- boolean anr = event.getImpl().isAnr(event);
- if (anr || promiseRejection) {
- cacheEvent(event, true);
- } else if (immutableConfig.getAttemptDeliveryOnCrash()) {
+ switch (event.getDeliveryStrategy()) {
+ case STORE_AND_SEND:
cacheAndSendSynchronously(event);
- } else {
+ break;
+ case STORE_ONLY:
cacheEvent(event, false);
- }
- } else if (callbackState.runOnSendTasks(event, logger)) {
- // Build the eventPayload
- String apiKey = event.getApiKey();
- EventPayload eventPayload = new EventPayload(apiKey, event, notifier, immutableConfig);
- deliverPayloadAsync(event, eventPayload);
+ break;
+ case SEND_IMMEDIATELY:
+ if (callbackState.runOnSendTasks(event, logger)) {
+ String apiKey = event.getApiKey();
+ EventPayload eventPayload = new EventPayload(
+ apiKey, event, notifier, immutableConfig);
+ deliverPayloadAsync(event, eventPayload);
+ }
+ break;
+ case STORE_AND_FLUSH:
+ default:
+ cacheEvent(event, true);
+ break;
}
}
diff --git a/bugsnag-android-core/src/main/java/com/bugsnag/android/DeliveryStrategy.kt b/bugsnag-android-core/src/main/java/com/bugsnag/android/DeliveryStrategy.kt
new file mode 100644
index 0000000000..b823b93c2a
--- /dev/null
+++ b/bugsnag-android-core/src/main/java/com/bugsnag/android/DeliveryStrategy.kt
@@ -0,0 +1,8 @@
+package com.bugsnag.android
+
+enum class DeliveryStrategy {
+ STORE_ONLY,
+ STORE_AND_FLUSH,
+ STORE_AND_SEND,
+ SEND_IMMEDIATELY,
+}
diff --git a/bugsnag-android-core/src/main/java/com/bugsnag/android/Event.java b/bugsnag-android-core/src/main/java/com/bugsnag/android/Event.java
index af6dd56e20..6bd4ef149c 100644
--- a/bugsnag-android-core/src/main/java/com/bugsnag/android/Event.java
+++ b/bugsnag-android-core/src/main/java/com/bugsnag/android/Event.java
@@ -1,5 +1,7 @@
package com.bugsnag.android;
+import static com.bugsnag.android.SeverityReason.REASON_PROMISE_REJECTION;
+
import com.bugsnag.android.internal.ImmutableConfig;
import com.bugsnag.android.internal.InternalMetrics;
@@ -575,4 +577,47 @@ void setRedactedKeys(Collection redactedKeys) {
void setInternalMetrics(InternalMetrics metrics) {
impl.setInternalMetrics(metrics);
}
+
+ /**
+ * Returns the delivery strategy for this event, which determines how the event
+ * should be delivered to the Bugsnag API.
+ *
+ * @return the delivery strategy, or null if no specific strategy is set
+ */
+ @NonNull
+ public DeliveryStrategy getDeliveryStrategy() {
+ if (impl.getDeliveryStrategy() != null) {
+ return impl.getDeliveryStrategy();
+ }
+
+ if (getImpl().getOriginalUnhandled()) {
+ String severityReasonType = getImpl().getSeverityReasonType();
+ boolean promiseRejection = REASON_PROMISE_REJECTION.equals(severityReasonType);
+ boolean anr = getImpl().isAnr(this);
+ if (anr || promiseRejection) {
+ return DeliveryStrategy.STORE_AND_FLUSH;
+ } else if (getImpl().isAttemptDeliveryOnCrash()) {
+ return DeliveryStrategy.STORE_AND_SEND;
+ } else {
+ return DeliveryStrategy.STORE_ONLY;
+ }
+ } else {
+ return DeliveryStrategy.SEND_IMMEDIATELY;
+ }
+ }
+
+ /**
+ * Sets the delivery strategy for this event, which determines how the event
+ * should be delivered to the Bugsnag API. This allows customization of delivery
+ * behavior on a per-event basis.
+ *
+ * @param deliveryStrategy the delivery strategy to use for this event
+ */
+ public void setDeliveryStrategy(@NonNull DeliveryStrategy deliveryStrategy) {
+ if (deliveryStrategy != null) {
+ impl.setDeliveryStrategy(deliveryStrategy);
+ } else {
+ logNull("deliveryStrategy");
+ }
+ }
}
diff --git a/bugsnag-android-core/src/main/java/com/bugsnag/android/EventInternal.kt b/bugsnag-android-core/src/main/java/com/bugsnag/android/EventInternal.kt
index da1c3fcf33..4ebc2ad65c 100644
--- a/bugsnag-android-core/src/main/java/com/bugsnag/android/EventInternal.kt
+++ b/bugsnag-android-core/src/main/java/com/bugsnag/android/EventInternal.kt
@@ -34,7 +34,8 @@ internal class EventInternal : FeatureFlagAware, JsonStream.Streamable, Metadata
severityReason,
ThreadState(originalError, severityReason.unhandled, config).threads,
User(),
- config.redactedKeys.toSet()
+ config.redactedKeys.toSet(),
+ config.attemptDeliveryOnCrash
)
internal constructor(
@@ -50,7 +51,8 @@ internal class EventInternal : FeatureFlagAware, JsonStream.Streamable, Metadata
severityReason: SeverityReason = SeverityReason.newInstance(SeverityReason.REASON_HANDLED_EXCEPTION),
threads: MutableList = mutableListOf(),
user: User = User(),
- redactionKeys: Set? = null
+ redactionKeys: Set? = null,
+ isAttemptDeliveryOnCrash: Boolean = false
) {
this.logger = logger
this.apiKey = apiKey
@@ -64,7 +66,7 @@ internal class EventInternal : FeatureFlagAware, JsonStream.Streamable, Metadata
this.severityReason = severityReason
this.threads = threads
this.userImpl = user
-
+ this.isAttemptDeliveryOnCrash = isAttemptDeliveryOnCrash
redactionKeys?.let {
this.redactedKeys = it
}
@@ -76,6 +78,8 @@ internal class EventInternal : FeatureFlagAware, JsonStream.Streamable, Metadata
val logger: Logger
val metadata: Metadata
val featureFlags: FeatureFlags
+ val isAttemptDeliveryOnCrash: Boolean
+
private val discardClasses: Set
internal var projectPackages: Collection
@@ -123,6 +127,8 @@ internal class EventInternal : FeatureFlagAware, JsonStream.Streamable, Metadata
var traceCorrelation: TraceCorrelation? = null
+ var deliveryStrategy: DeliveryStrategy? = null
+
fun getUnhandledOverridden(): Boolean = severityReason.unhandledOverridden
fun getOriginalUnhandled(): Boolean = severityReason.originalUnhandled
diff --git a/bugsnag-android-core/src/test/java/com/bugsnag/android/DeliveryDelegateTest.kt b/bugsnag-android-core/src/test/java/com/bugsnag/android/DeliveryDelegateTest.kt
index c81287c873..179c086ee9 100644
--- a/bugsnag-android-core/src/test/java/com/bugsnag/android/DeliveryDelegateTest.kt
+++ b/bugsnag-android-core/src/test/java/com/bugsnag/android/DeliveryDelegateTest.kt
@@ -42,7 +42,9 @@ internal class DeliveryDelegateTest {
notifier,
BackgroundTaskService()
)
- event.session = Session("123", Date(), User(null, null, null), false, notifier, NoopLogger, apiKey)
+ event.session = Session(
+ "123", Date(), User(null, null, null), false, notifier, NoopLogger, apiKey
+ )
}
@Test
@@ -63,6 +65,8 @@ internal class DeliveryDelegateTest {
assertEquals(0, event.session!!.handledCount)
assertEquals("BUGSNAG_API_KEY", event.session!!.apiKey)
+
+ assertEquals(DeliveryStrategy.STORE_ONLY, event.deliveryStrategy)
}
@Test
@@ -71,7 +75,9 @@ internal class DeliveryDelegateTest {
SeverityReason.REASON_HANDLED_EXCEPTION
)
val event = Event(RuntimeException("Whoops!"), config, state, NoopLogger)
- event.session = Session("123", Date(), User(null, null, null), false, notifier, NoopLogger, apiKey)
+ event.session = Session(
+ "123", Date(), User(null, null, null), false, notifier, NoopLogger, apiKey
+ )
var msg: StateEvent.NotifyHandled? = null
deliveryDelegate.addObserver(
@@ -87,6 +93,8 @@ internal class DeliveryDelegateTest {
// check session count incremented
assertEquals(0, event.session!!.unhandledCount)
assertEquals(1, event.session!!.handledCount)
+
+ assertEquals(DeliveryStrategy.SEND_IMMEDIATELY, event.deliveryStrategy)
}
@Test
@@ -107,6 +115,8 @@ internal class DeliveryDelegateTest {
// verify no payload was sent for an Event with no errors
assertNull(msg)
+
+ assertEquals(DeliveryStrategy.SEND_IMMEDIATELY, event.deliveryStrategy)
}
@Test
diff --git a/bugsnag-android-core/src/test/java/com/bugsnag/android/EventDeliveryStrategyTest.kt b/bugsnag-android-core/src/test/java/com/bugsnag/android/EventDeliveryStrategyTest.kt
new file mode 100644
index 0000000000..9dd02e0669
--- /dev/null
+++ b/bugsnag-android-core/src/test/java/com/bugsnag/android/EventDeliveryStrategyTest.kt
@@ -0,0 +1,99 @@
+package com.bugsnag.android
+
+import com.bugsnag.android.BugsnagTestUtils.generateImmutableConfig
+import org.junit.Assert.assertEquals
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.junit.MockitoJUnitRunner
+import java.util.Date
+
+@RunWith(MockitoJUnitRunner::class)
+internal class EventDeliveryStrategyTest {
+
+ private val apiKey = "BUGSNAG_API_KEY"
+ private val notifier = Notifier()
+ private val config = generateImmutableConfig()
+ private var testException: RuntimeException? = null
+ private val event = Event(
+ RuntimeException("Whoops!"), config,
+ SeverityReason.newInstance(
+ SeverityReason.REASON_UNHANDLED_EXCEPTION
+ ),
+ NoopLogger
+ )
+
+ @Before
+ fun setUp() {
+ testException = RuntimeException("Example message")
+ event.session = Session(
+ "123", Date(), User(null, null, null), false, notifier, NoopLogger, apiKey
+ )
+ }
+
+ @Test
+ fun testGenerateUnhandledReport() {
+ assertEquals(DeliveryStrategy.STORE_ONLY, event.deliveryStrategy)
+ event.deliveryStrategy = DeliveryStrategy.SEND_IMMEDIATELY
+ assertEquals(DeliveryStrategy.SEND_IMMEDIATELY, event.deliveryStrategy)
+ }
+
+ @Test
+ fun testANRStoreEventAndFlush() {
+ val anrEvent = Event(
+ testException, config,
+ SeverityReason.newInstance(SeverityReason.REASON_ANR),
+ NoopLogger
+ )
+ anrEvent.getErrors().get(0).setErrorClass("ANR")
+ assertEquals(DeliveryStrategy.STORE_AND_FLUSH, anrEvent.getDeliveryStrategy())
+ }
+
+ @Test
+ fun testPromiseRejectionEvetStoreAndFlush() {
+ val promiseRejectionEvent = Event(
+ testException, config,
+ SeverityReason.newInstance(SeverityReason.REASON_PROMISE_REJECTION),
+ NoopLogger
+ )
+ assertEquals(DeliveryStrategy.STORE_AND_FLUSH, promiseRejectionEvent.getDeliveryStrategy())
+ }
+
+ @Test
+ fun testANRWithModifiedErrorClassStoreAndFlush() {
+ val modifiedAnrEvent = Event(
+ testException,
+ config,
+ SeverityReason.newInstance(SeverityReason.REASON_PROMISE_REJECTION),
+ NoopLogger
+ )
+ modifiedAnrEvent.getErrors().get(0).setErrorClass("ANR")
+ assertEquals(DeliveryStrategy.STORE_AND_FLUSH, modifiedAnrEvent.getDeliveryStrategy())
+ }
+
+ @Test
+ fun testGenerateHandledReport() {
+ val state = SeverityReason.newInstance(
+ SeverityReason.REASON_HANDLED_EXCEPTION
+ )
+ val event = Event(RuntimeException("Whoops!"), config, state, NoopLogger)
+ event.session = Session(
+ "123", Date(), User(null, null, null), false, notifier, NoopLogger, apiKey
+ )
+ assertEquals(DeliveryStrategy.SEND_IMMEDIATELY, event.deliveryStrategy)
+ }
+
+ @Test
+ fun testDeliveryStrategyStoreAndSend() {
+ val configuration = Configuration(apiKey)
+ val newConfig = generateImmutableConfig(configuration).apply {
+ configuration.setAttemptDeliveryOnCrash(true)
+ }
+ val unhandledEvent = Event(
+ testException, newConfig,
+ SeverityReason.newInstance(SeverityReason.REASON_UNHANDLED_EXCEPTION),
+ NoopLogger
+ )
+ assertEquals(DeliveryStrategy.STORE_ONLY, unhandledEvent.getDeliveryStrategy())
+ }
+}
diff --git a/bugsnag-plugin-android-exitinfo/detekt-baseline.xml b/bugsnag-plugin-android-exitinfo/detekt-baseline.xml
index eeeaea2a8d..287886c1e6 100644
--- a/bugsnag-plugin-android-exitinfo/detekt-baseline.xml
+++ b/bugsnag-plugin-android-exitinfo/detekt-baseline.xml
@@ -4,7 +4,6 @@
CyclomaticComplexMethod:CodeDescriptions.kt$@RequiresApi(Build.VERSION_CODES.R) @SuppressLint("SwitchIntDef") @Suppress("DEPRECATION") internal fun importanceDescriptionOf(exitInfo: ApplicationExitInfo)
CyclomaticComplexMethod:CodeDescriptions.kt$@RequiresApi(Build.VERSION_CODES.R) internal fun exitReasonOf(exitInfo: ApplicationExitInfo)
- LongParameterList:TombstoneParser.kt$TombstoneParser$( exitInfo: ApplicationExitInfo, listOpenFds: Boolean, includeLogcat: Boolean, threadConsumer: (BugsnagThread) -> Unit, fileDescriptorConsumer: (Int, String, String) -> Unit, logcatConsumer: (String) -> Unit )
MagicNumber:TraceParser.kt$TraceParser$16
MagicNumber:TraceParser.kt$TraceParser$3
MaxLineLength:TraceParserInvalidStackframesTest.kt$TraceParserInvalidStackframesTest.Companion$"#01 pc 0000000000000c5c /data/app/~~sKQbJGqVJA5glcnvEjZCMg==/com.example.bugsnag.android-fVuoJh5GpAL7sRAeI3vjSw==/lib/arm64/libentrypoint.so "