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 "