diff --git a/CHANGELOG.md b/CHANGELOG.md index f140ed0104..d01509630f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,14 @@ # Changelog +## 6.9.0 (2024-11-07) + +### Enhancements + +* Native crashes will now identify the crashing/error reporting thread + [#2087](https://github.com/bugsnag/bugsnag-android/pull/2087) +* A new `OkHttpDelivery` delivery implementation is available in the [bugsnag-plugin-android-okhttp](bugsnag-plugin-android-okhttp) module + [#2092](https://github.com/bugsnag/bugsnag-android/pull/2092) + ## 6.8.0 (2024-09-30) ### Enhancements diff --git a/bugsnag-android-core/api/bugsnag-android-core.api b/bugsnag-android-core/api/bugsnag-android-core.api index 2ffe8e2f0f..cc0f2f5493 100644 --- a/bugsnag-android-core/api/bugsnag-android-core.api +++ b/bugsnag-android-core/api/bugsnag-android-core.api @@ -236,6 +236,15 @@ public class com/bugsnag/android/Configuration : com/bugsnag/android/CallbackAwa public fun setVersionCode (Ljava/lang/Integer;)V } +public abstract interface class com/bugsnag/android/Deliverable { + public abstract fun getIntegrityToken ()Ljava/lang/String; + public abstract fun toByteArray ()[B +} + +public final class com/bugsnag/android/Deliverable$DefaultImpls { + public static fun getIntegrityToken (Lcom/bugsnag/android/Deliverable;)Ljava/lang/String; +} + public abstract interface class com/bugsnag/android/Delivery { public abstract fun deliver (Lcom/bugsnag/android/EventPayload;Lcom/bugsnag/android/DeliveryParams;)Lcom/bugsnag/android/DeliveryStatus; public abstract fun deliver (Lcom/bugsnag/android/Session;Lcom/bugsnag/android/DeliveryParams;)Lcom/bugsnag/android/DeliveryStatus; @@ -248,13 +257,19 @@ public final class com/bugsnag/android/DeliveryParams { } public final class com/bugsnag/android/DeliveryStatus : java/lang/Enum { + public static final field Companion Lcom/bugsnag/android/DeliveryStatus$Companion; public static final field DELIVERED Lcom/bugsnag/android/DeliveryStatus; public static final field FAILURE Lcom/bugsnag/android/DeliveryStatus; public static final field UNDELIVERED Lcom/bugsnag/android/DeliveryStatus; + public static final fun forHttpResponseCode (I)Lcom/bugsnag/android/DeliveryStatus; public static fun valueOf (Ljava/lang/String;)Lcom/bugsnag/android/DeliveryStatus; public static fun values ()[Lcom/bugsnag/android/DeliveryStatus; } +public final class com/bugsnag/android/DeliveryStatus$Companion { + public final fun forHttpResponseCode (I)Lcom/bugsnag/android/DeliveryStatus; +} + 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; @@ -395,13 +410,23 @@ public class com/bugsnag/android/Event : com/bugsnag/android/FeatureFlagAware, c protected fun updateSeverityReason (Ljava/lang/String;)V } -public final class com/bugsnag/android/EventPayload : com/bugsnag/android/JsonStream$Streamable { +public final class com/bugsnag/android/EventPayload : com/bugsnag/android/Deliverable, com/bugsnag/android/JsonStream$Streamable { + public static final field Companion Lcom/bugsnag/android/EventPayload$Companion; + public static final field DEFAULT_MAX_PAYLOAD_SIZE I public fun (Ljava/lang/String;Lcom/bugsnag/android/Event;Lcom/bugsnag/android/Notifier;Lcom/bugsnag/android/internal/ImmutableConfig;)V public fun (Ljava/lang/String;Lcom/bugsnag/android/Notifier;Lcom/bugsnag/android/internal/ImmutableConfig;)V public final fun getApiKey ()Ljava/lang/String; public final fun getEvent ()Lcom/bugsnag/android/Event; + public fun getIntegrityToken ()Ljava/lang/String; public final fun setApiKey (Ljava/lang/String;)V + public fun toByteArray ()[B public fun toStream (Lcom/bugsnag/android/JsonStream;)V + public final fun trimToSize ()Lcom/bugsnag/android/EventPayload; + public final fun trimToSize (I)Lcom/bugsnag/android/EventPayload; + public static synthetic fun trimToSize$default (Lcom/bugsnag/android/EventPayload;IILjava/lang/Object;)Lcom/bugsnag/android/EventPayload; +} + +public final class com/bugsnag/android/EventPayload$Companion { } public final class com/bugsnag/android/FeatureFlag : java/util/Map$Entry { @@ -585,17 +610,19 @@ public abstract interface class com/bugsnag/android/Plugin { public abstract fun unload ()V } -public final class com/bugsnag/android/Session : com/bugsnag/android/JsonStream$Streamable, com/bugsnag/android/UserAware { +public final class com/bugsnag/android/Session : com/bugsnag/android/Deliverable, com/bugsnag/android/JsonStream$Streamable, com/bugsnag/android/UserAware { public fun getApiKey ()Ljava/lang/String; public fun getApp ()Lcom/bugsnag/android/App; public fun getDevice ()Lcom/bugsnag/android/Device; public fun getId ()Ljava/lang/String; + public fun getIntegrityToken ()Ljava/lang/String; public fun getStartedAt ()Ljava/util/Date; public fun getUser ()Lcom/bugsnag/android/User; public fun setApiKey (Ljava/lang/String;)V public fun setId (Ljava/lang/String;)V public fun setStartedAt (Ljava/util/Date;)V public fun setUser (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)V + public fun toByteArray ()[B public fun toStream (Lcom/bugsnag/android/JsonStream;)V } @@ -876,7 +903,7 @@ public final class com/bugsnag/android/internal/DateUtils { } public final class com/bugsnag/android/internal/ImmutableConfig { - public fun (Ljava/lang/String;ZLcom/bugsnag/android/ErrorTypes;ZLcom/bugsnag/android/ThreadSendPolicy;Ljava/util/Collection;Ljava/util/Collection;Ljava/util/Collection;Ljava/util/Set;Ljava/util/Set;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/Integer;Ljava/lang/String;Lcom/bugsnag/android/Delivery;Lcom/bugsnag/android/EndpointConfiguration;ZJLcom/bugsnag/android/Logger;IIIIJLkotlin/Lazy;ZZZLandroid/content/pm/PackageInfo;Landroid/content/pm/ApplicationInfo;Ljava/util/Collection;)V + public fun (Ljava/lang/String;ZLcom/bugsnag/android/ErrorTypes;ZLcom/bugsnag/android/ThreadSendPolicy;Ljava/util/Collection;Ljava/util/Collection;Ljava/util/Collection;Ljava/util/Set;Ljava/util/Set;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/Integer;Ljava/lang/String;Lcom/bugsnag/android/Delivery;Lcom/bugsnag/android/EndpointConfiguration;ZJLcom/bugsnag/android/Logger;IIIIIJLkotlin/Lazy;ZZZLandroid/content/pm/PackageInfo;Landroid/content/pm/ApplicationInfo;Ljava/util/Collection;)V public final fun component1 ()Ljava/lang/String; public final fun component10 ()Ljava/util/Set; public final fun component11 ()Ljava/lang/String; @@ -894,23 +921,24 @@ public final class com/bugsnag/android/internal/ImmutableConfig { public final fun component22 ()I public final fun component23 ()I public final fun component24 ()I - public final fun component25 ()J - public final fun component26 ()Lkotlin/Lazy; - public final fun component27 ()Z + public final fun component25 ()I + public final fun component26 ()J + public final fun component27 ()Lkotlin/Lazy; public final fun component28 ()Z public final fun component29 ()Z public final fun component3 ()Lcom/bugsnag/android/ErrorTypes; - public final fun component30 ()Landroid/content/pm/PackageInfo; - public final fun component31 ()Landroid/content/pm/ApplicationInfo; - public final fun component32 ()Ljava/util/Collection; + public final fun component30 ()Z + public final fun component31 ()Landroid/content/pm/PackageInfo; + public final fun component32 ()Landroid/content/pm/ApplicationInfo; + public final fun component33 ()Ljava/util/Collection; public final fun component4 ()Z public final fun component5 ()Lcom/bugsnag/android/ThreadSendPolicy; public final fun component6 ()Ljava/util/Collection; public final fun component7 ()Ljava/util/Collection; public final fun component8 ()Ljava/util/Collection; public final fun component9 ()Ljava/util/Set; - public final fun copy (Ljava/lang/String;ZLcom/bugsnag/android/ErrorTypes;ZLcom/bugsnag/android/ThreadSendPolicy;Ljava/util/Collection;Ljava/util/Collection;Ljava/util/Collection;Ljava/util/Set;Ljava/util/Set;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/Integer;Ljava/lang/String;Lcom/bugsnag/android/Delivery;Lcom/bugsnag/android/EndpointConfiguration;ZJLcom/bugsnag/android/Logger;IIIIJLkotlin/Lazy;ZZZLandroid/content/pm/PackageInfo;Landroid/content/pm/ApplicationInfo;Ljava/util/Collection;)Lcom/bugsnag/android/internal/ImmutableConfig; - public static synthetic fun copy$default (Lcom/bugsnag/android/internal/ImmutableConfig;Ljava/lang/String;ZLcom/bugsnag/android/ErrorTypes;ZLcom/bugsnag/android/ThreadSendPolicy;Ljava/util/Collection;Ljava/util/Collection;Ljava/util/Collection;Ljava/util/Set;Ljava/util/Set;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/Integer;Ljava/lang/String;Lcom/bugsnag/android/Delivery;Lcom/bugsnag/android/EndpointConfiguration;ZJLcom/bugsnag/android/Logger;IIIIJLkotlin/Lazy;ZZZLandroid/content/pm/PackageInfo;Landroid/content/pm/ApplicationInfo;Ljava/util/Collection;ILjava/lang/Object;)Lcom/bugsnag/android/internal/ImmutableConfig; + public final fun copy (Ljava/lang/String;ZLcom/bugsnag/android/ErrorTypes;ZLcom/bugsnag/android/ThreadSendPolicy;Ljava/util/Collection;Ljava/util/Collection;Ljava/util/Collection;Ljava/util/Set;Ljava/util/Set;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/Integer;Ljava/lang/String;Lcom/bugsnag/android/Delivery;Lcom/bugsnag/android/EndpointConfiguration;ZJLcom/bugsnag/android/Logger;IIIIIJLkotlin/Lazy;ZZZLandroid/content/pm/PackageInfo;Landroid/content/pm/ApplicationInfo;Ljava/util/Collection;)Lcom/bugsnag/android/internal/ImmutableConfig; + public static synthetic fun copy$default (Lcom/bugsnag/android/internal/ImmutableConfig;Ljava/lang/String;ZLcom/bugsnag/android/ErrorTypes;ZLcom/bugsnag/android/ThreadSendPolicy;Ljava/util/Collection;Ljava/util/Collection;Ljava/util/Collection;Ljava/util/Set;Ljava/util/Set;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/Integer;Ljava/lang/String;Lcom/bugsnag/android/Delivery;Lcom/bugsnag/android/EndpointConfiguration;ZJLcom/bugsnag/android/Logger;IIIIIJLkotlin/Lazy;ZZZLandroid/content/pm/PackageInfo;Landroid/content/pm/ApplicationInfo;Ljava/util/Collection;IILjava/lang/Object;)Lcom/bugsnag/android/internal/ImmutableConfig; public fun equals (Ljava/lang/Object;)Z public final fun getApiKey ()Ljava/lang/String; public final fun getAppInfo ()Landroid/content/pm/ApplicationInfo; @@ -933,6 +961,7 @@ public final class com/bugsnag/android/internal/ImmutableConfig { public final fun getMaxPersistedEvents ()I public final fun getMaxPersistedSessions ()I public final fun getMaxReportedThreads ()I + public final fun getMaxStringValueLength ()I public final fun getPackageInfo ()Landroid/content/pm/PackageInfo; public final fun getPersistUser ()Z public final fun getPersistenceDirectory ()Lkotlin/Lazy; diff --git a/bugsnag-android-core/detekt-baseline.xml b/bugsnag-android-core/detekt-baseline.xml index e4e75d4c39..43eaee71f8 100644 --- a/bugsnag-android-core/detekt-baseline.xml +++ b/bugsnag-android-core/detekt-baseline.xml @@ -4,7 +4,7 @@ CyclomaticComplexMethod:AppDataCollector.kt$AppDataCollector$@SuppressLint("SwitchIntDef") @Suppress("DEPRECATION") private fun getProcessImportance(): String? CyclomaticComplexMethod:ConfigInternal.kt$ConfigInternal$fun getConfigDifferences(): Map<String, Any> - ImplicitDefaultLocale:DeliveryHeaders.kt$String.format("%02x", byte) + ImplicitDefaultLocale:Deliverable.kt$Deliverable$String.format("%02x", byte) LongParameterList:App.kt$App$( /** * The architecture of the running application binary */ var binaryArch: String?, /** * The package name of the application */ var id: String?, /** * The release stage set in [Configuration.releaseStage] */ var releaseStage: String?, /** * The version of the application set in [Configuration.version] */ var version: String?, /** The revision ID from the manifest (React Native apps only) */ var codeBundleId: String?, /** * The unique identifier for the build of the application set in [Configuration.buildUuid] */ var buildUuid: String?, /** * The application type set in [Configuration#version] */ var type: String?, /** * The version code of the application set in [Configuration.versionCode] */ var versionCode: Number? ) LongParameterList:AppDataCollector.kt$AppDataCollector$( appContext: Context, private val packageManager: PackageManager?, private val config: ImmutableConfig, private val sessionTracker: SessionTracker, private val activityManager: ActivityManager?, private val launchCrashTracker: LaunchCrashTracker, private val memoryTrimState: MemoryTrimState ) LongParameterList:AppWithState.kt$AppWithState$( binaryArch: String?, id: String?, releaseStage: String?, version: String?, codeBundleId: String?, buildUuid: String?, type: String?, versionCode: Number?, /** * The number of milliseconds the application was running before the event occurred */ var duration: Number?, /** * The number of milliseconds the application was running in the foreground before the * event occurred */ var durationInForeground: Number?, /** * Whether the application was in the foreground when the event occurred */ var inForeground: Boolean?, /** * Whether the application was launching when the event occurred */ var isLaunching: Boolean? ) @@ -23,9 +23,9 @@ MagicNumber:BugsnagEventMapper.kt$BugsnagEventMapper$16 MagicNumber:BugsnagEventMapper.kt$BugsnagEventMapper$32 MagicNumber:BugsnagEventMapper.kt$BugsnagEventMapper$56 - MagicNumber:DefaultDelivery.kt$DefaultDelivery$299 - MagicNumber:DefaultDelivery.kt$DefaultDelivery$429 - MagicNumber:DefaultDelivery.kt$DefaultDelivery$499 + MagicNumber:DeliveryStatus.kt$DeliveryStatus.Companion$299 + MagicNumber:DeliveryStatus.kt$DeliveryStatus.Companion$429 + MagicNumber:DeliveryStatus.kt$DeliveryStatus.Companion$499 MagicNumber:EventStore.kt$EventStore$60 MagicNumber:JsonHelper.kt$JsonHelper$0xff MagicNumber:JsonHelper.kt$JsonHelper$1000 @@ -39,7 +39,6 @@ MaxLineLength:LastRunInfo.kt$LastRunInfo$return "LastRunInfo(consecutiveLaunchCrashes=$consecutiveLaunchCrashes, crashed=$crashed, crashedDuringLaunch=$crashedDuringLaunch)" MaxLineLength:ThreadState.kt$ThreadState$"[${allThreads.size - maxThreadCount} threads omitted as the maxReportedThreads limit ($maxThreadCount) was exceeded]" NestedBlockDepth:FileStore.kt$FileStore$fun deleteStoredFiles(storedFiles: Collection<File>?) - NestedBlockDepth:FileStore.kt$FileStore$fun discardOldestFileIfNeeded() NestedBlockDepth:FileStore.kt$FileStore$fun findStoredFiles(): MutableList<File> NestedBlockDepth:JsonHelper.kt$JsonHelper$fun jsonToLong(value: Any?): Long? ProtectedMemberInFinalClass:ConfigInternal.kt$ConfigInternal$protected val plugins = HashSet<Plugin>() @@ -47,8 +46,7 @@ ProtectedMemberInFinalClass:EventInternal.kt$EventInternal$protected fun shouldDiscardClass(): Boolean ProtectedMemberInFinalClass:EventInternal.kt$EventInternal$protected fun updateSeverityInternal(severity: Severity) ProtectedMemberInFinalClass:EventInternal.kt$EventInternal$protected fun updateSeverityReason(@SeverityReason.SeverityReasonType reason: String) - ReturnCount:DefaultDelivery.kt$DefaultDelivery$fun deliver( urlString: String, json: ByteArray, headers: Map<String, String?> ): DeliveryStatus - SpreadOperator:FileStore.kt$FileStore$(*listFiles) + ReturnCount:DefaultDelivery.kt$DefaultDelivery$fun deliver( urlString: String, json: ByteArray, integrity: String?, headers: Map<String, String?> ): DeliveryStatus SwallowedException:ActivityBreadcrumbCollector.kt$ActivityBreadcrumbCollector$re: Exception SwallowedException:AppDataCollector.kt$AppDataCollector$e: Exception SwallowedException:BugsnagEventMapper.kt$BugsnagEventMapper$pe: IllegalArgumentException diff --git a/bugsnag-android-core/src/androidTest/java/com/bugsnag/android/FileStoreTest.kt b/bugsnag-android-core/src/androidTest/java/com/bugsnag/android/FileStoreTest.kt index d2c87b053d..6c4733ac54 100644 --- a/bugsnag-android-core/src/androidTest/java/com/bugsnag/android/FileStoreTest.kt +++ b/bugsnag-android-core/src/androidTest/java/com/bugsnag/android/FileStoreTest.kt @@ -2,13 +2,11 @@ package com.bugsnag.android import android.app.Application import androidx.test.core.app.ApplicationProvider -import com.bugsnag.android.EventStore.Companion.EVENT_COMPARATOR import org.junit.Assert.assertEquals import org.junit.Test import org.junit.runner.RunWith import org.mockito.junit.MockitoJUnitRunner import java.io.File -import java.util.Comparator @RunWith(MockitoJUnitRunner::class) class FileStoreTest { @@ -18,7 +16,7 @@ class FileStoreTest { val delegate = CustomDelegate() val dir = File(ApplicationProvider.getApplicationContext().cacheDir, "tmp") - val store = CustomFileStore(dir, 1, EVENT_COMPARATOR, delegate) + val store = CustomFileStore(dir, 1, delegate) val exc = RuntimeException("Whoops") store.write(CustomStreamable(exc)) @@ -49,8 +47,7 @@ class CustomStreamable(private val exc: Throwable) : JsonStream.Streamable { internal class CustomFileStore( folder: File, maxStoreCount: Int, - comparator: Comparator, delegate: Delegate? -) : FileStore(folder, maxStoreCount, comparator, NoopLogger, delegate) { +) : FileStore(folder, maxStoreCount, NoopLogger, delegate) { override fun getFilename(obj: Any?) = "foo.json" } diff --git a/bugsnag-android-core/src/main/java/com/bugsnag/android/DefaultDelivery.kt b/bugsnag-android-core/src/main/java/com/bugsnag/android/DefaultDelivery.kt index 72c8a9200f..a186936579 100644 --- a/bugsnag-android-core/src/main/java/com/bugsnag/android/DefaultDelivery.kt +++ b/bugsnag-android-core/src/main/java/com/bugsnag/android/DefaultDelivery.kt @@ -4,69 +4,27 @@ import android.net.TrafficStats import com.bugsnag.android.internal.JsonHelper import java.io.IOException import java.net.HttpURLConnection -import java.net.HttpURLConnection.HTTP_BAD_REQUEST -import java.net.HttpURLConnection.HTTP_CLIENT_TIMEOUT -import java.net.HttpURLConnection.HTTP_OK import java.net.URL internal class DefaultDelivery( private val connectivity: Connectivity?, - private val apiKey: String, - private val maxStringValueLength: Int, private val logger: Logger ) : Delivery { - companion object { - // 1MB with some fiddle room in case of encoding overhead - const val maxPayloadSize = 999700 - } - override fun deliver(payload: Session, deliveryParams: DeliveryParams): DeliveryStatus { val status = deliver( deliveryParams.endpoint, JsonHelper.serialize(payload), + payload.integrityToken, deliveryParams.headers ) logger.i("Session API request finished with status $status") return status } - private fun serializePayload(payload: EventPayload): ByteArray { - var json = JsonHelper.serialize(payload) - if (json.size <= maxPayloadSize) { - return json - } - - var event = payload.event - if (event == null) { - event = MarshalledEventSource(payload.eventFile!!, apiKey, logger).invoke() - payload.event = event - payload.apiKey = apiKey - } - - val (itemsTrimmed, dataTrimmed) = event.impl.trimMetadataStringsTo(maxStringValueLength) - event.impl.internalMetrics.setMetadataTrimMetrics( - itemsTrimmed, - dataTrimmed - ) - - json = JsonHelper.serialize(payload) - if (json.size <= maxPayloadSize) { - return json - } - - val breadcrumbAndBytesRemovedCounts = - event.impl.trimBreadcrumbsBy(json.size - maxPayloadSize) - event.impl.internalMetrics.setBreadcrumbTrimMetrics( - breadcrumbAndBytesRemovedCounts.itemsTrimmed, - breadcrumbAndBytesRemovedCounts.dataTrimmed - ) - return JsonHelper.serialize(payload) - } - override fun deliver(payload: EventPayload, deliveryParams: DeliveryParams): DeliveryStatus { - val json = serializePayload(payload) - val status = deliver(deliveryParams.endpoint, json, deliveryParams.headers) + val json = payload.trimToSize().toByteArray() + val status = deliver(deliveryParams.endpoint, json, payload.integrityToken, deliveryParams.headers) logger.i("Error API request finished with status $status") return status } @@ -74,6 +32,7 @@ internal class DefaultDelivery( fun deliver( urlString: String, json: ByteArray, + integrity: String?, headers: Map ): DeliveryStatus { @@ -84,11 +43,11 @@ internal class DefaultDelivery( var conn: HttpURLConnection? = null try { - conn = makeRequest(URL(urlString), json, headers) + conn = makeRequest(URL(urlString), json, integrity, headers) // End the request, get the response code val responseCode = conn.responseCode - val status = getDeliveryStatus(responseCode) + val status = DeliveryStatus.forHttpResponseCode(responseCode) logRequestInfo(responseCode, conn, status) return status } catch (oom: OutOfMemoryError) { @@ -111,6 +70,7 @@ internal class DefaultDelivery( private fun makeRequest( url: URL, json: ByteArray, + integrity: String?, headers: Map ): HttpURLConnection { val conn = url.openConnection() as HttpURLConnection @@ -120,8 +80,7 @@ internal class DefaultDelivery( // https://developer.android.com/reference/java/net/HttpURLConnection conn.setFixedLengthStreamingMode(json.size) - // calculate the SHA-1 digest and add all other headers - computeSha1Digest(json)?.let { digest -> + integrity?.let { digest -> conn.addRequestProperty(HEADER_BUGSNAG_INTEGRITY, digest) } headers.forEach { (key, value) -> @@ -159,17 +118,4 @@ internal class DefaultDelivery( } } } - - internal fun getDeliveryStatus(responseCode: Int): DeliveryStatus { - return when { - responseCode in HTTP_OK..299 -> DeliveryStatus.DELIVERED - isUnrecoverableStatusCode(responseCode) -> DeliveryStatus.FAILURE - else -> DeliveryStatus.UNDELIVERED - } - } - - private fun isUnrecoverableStatusCode(responseCode: Int) = - responseCode in HTTP_BAD_REQUEST..499 && // 400-499 are considered unrecoverable - responseCode != HTTP_CLIENT_TIMEOUT && // except for 408 - responseCode != 429 // and 429 } diff --git a/bugsnag-android-core/src/main/java/com/bugsnag/android/Deliverable.kt b/bugsnag-android-core/src/main/java/com/bugsnag/android/Deliverable.kt new file mode 100644 index 0000000000..c41da5de5e --- /dev/null +++ b/bugsnag-android-core/src/main/java/com/bugsnag/android/Deliverable.kt @@ -0,0 +1,39 @@ +package com.bugsnag.android + +import java.io.IOException +import java.security.DigestOutputStream +import java.security.MessageDigest + +/** + * Denotes objects that are expected to be delivered over a network. + */ +interface Deliverable { + /** + * Return the byte representation of this `Deliverable`. + */ + @Throws(IOException::class) + fun toByteArray(): ByteArray + + /** + * The value of the "Bugsnag-Integrity" HTTP header returned as a String. This value is used + * to validate the payload and is expected by the standard BugSnag servers. + */ + val integrityToken: String? + get() { + runCatching { + val shaDigest = MessageDigest.getInstance("SHA-1") + val builder = StringBuilder("sha1 ") + + // Pipe the object through a no-op output stream + DigestOutputStream(NullOutputStream(), shaDigest).use { stream -> + stream.buffered().use { writer -> + writer.write(toByteArray()) + } + shaDigest.digest().forEach { byte -> + builder.append(String.format("%02x", byte)) + } + } + return builder.toString() + }.getOrElse { return null } + } +} diff --git a/bugsnag-android-core/src/main/java/com/bugsnag/android/DeliveryHeaders.kt b/bugsnag-android-core/src/main/java/com/bugsnag/android/DeliveryHeaders.kt index 89df18055d..c5d2d7fee7 100644 --- a/bugsnag-android-core/src/main/java/com/bugsnag/android/DeliveryHeaders.kt +++ b/bugsnag-android-core/src/main/java/com/bugsnag/android/DeliveryHeaders.kt @@ -2,8 +2,6 @@ package com.bugsnag.android import com.bugsnag.android.internal.DateUtils import java.io.OutputStream -import java.security.DigestOutputStream -import java.security.MessageDigest import java.util.Date private const val HEADER_API_PAYLOAD_VERSION = "Bugsnag-Payload-Version" @@ -60,24 +58,6 @@ internal fun sessionApiHeaders(apiKey: String): Map = mapOf( HEADER_BUGSNAG_SENT_AT to DateUtils.toIso8601(Date()) ) -internal fun computeSha1Digest(payload: ByteArray): String? { - runCatching { - val shaDigest = MessageDigest.getInstance("SHA-1") - val builder = StringBuilder("sha1 ") - - // Pipe the object through a no-op output stream - DigestOutputStream(NullOutputStream(), shaDigest).use { stream -> - stream.buffered().use { writer -> - writer.write(payload) - } - shaDigest.digest().forEach { byte -> - builder.append(String.format("%02x", byte)) - } - } - return builder.toString() - }.getOrElse { return null } -} - internal class NullOutputStream : OutputStream() { override fun write(b: Int) = Unit } diff --git a/bugsnag-android-core/src/main/java/com/bugsnag/android/DeliveryStatus.kt b/bugsnag-android-core/src/main/java/com/bugsnag/android/DeliveryStatus.kt index 9bbd4d8ccf..37c398805f 100644 --- a/bugsnag-android-core/src/main/java/com/bugsnag/android/DeliveryStatus.kt +++ b/bugsnag-android-core/src/main/java/com/bugsnag/android/DeliveryStatus.kt @@ -1,5 +1,9 @@ package com.bugsnag.android +import java.net.HttpURLConnection.HTTP_BAD_REQUEST +import java.net.HttpURLConnection.HTTP_CLIENT_TIMEOUT +import java.net.HttpURLConnection.HTTP_OK + /** * Return value for the status of a payload delivery. */ @@ -19,5 +23,19 @@ enum class DeliveryStatus { * * The payload was not delivered and should be deleted without attempting retry. */ - FAILURE + FAILURE; + + companion object { + @JvmStatic + fun forHttpResponseCode(responseCode: Int): DeliveryStatus { + return when { + responseCode in HTTP_OK..299 -> DELIVERED + responseCode in HTTP_BAD_REQUEST..499 && // 400-499 are considered unrecoverable + responseCode != HTTP_CLIENT_TIMEOUT && // except for 408 + responseCode != 429 -> FAILURE + + else -> UNDELIVERED + } + } + } } diff --git a/bugsnag-android-core/src/main/java/com/bugsnag/android/EventPayload.kt b/bugsnag-android-core/src/main/java/com/bugsnag/android/EventPayload.kt index 51f213016d..01431aea8c 100644 --- a/bugsnag-android-core/src/main/java/com/bugsnag/android/EventPayload.kt +++ b/bugsnag-android-core/src/main/java/com/bugsnag/android/EventPayload.kt @@ -1,6 +1,8 @@ package com.bugsnag.android +import androidx.annotation.VisibleForTesting import com.bugsnag.android.internal.ImmutableConfig +import com.bugsnag.android.internal.JsonHelper import java.io.File import java.io.IOException @@ -13,13 +15,20 @@ import java.io.IOException class EventPayload @JvmOverloads internal constructor( var apiKey: String?, event: Event? = null, - internal val eventFile: File? = null, + eventFile: File? = null, notifier: Notifier, private val config: ImmutableConfig -) : JsonStream.Streamable { +) : JsonStream.Streamable, Deliverable { - var event = event - internal set(value) { field = value } + var event: Event? = event + internal set + + internal var eventFile: File? = eventFile + private set + + private var cachedBytes: ByteArray? = null + + private val logger: Logger get() = config.logger internal val notifier = Notifier(notifier.name, notifier.version, notifier.url).apply { dependencies = notifier.dependencies.toMutableList() @@ -27,11 +36,64 @@ class EventPayload @JvmOverloads internal constructor( internal fun getErrorTypes(): Set { val event = this.event - return when { - event != null -> event.impl.getErrorTypesFromStackframes() - eventFile != null -> EventFilenameInfo.fromFile(eventFile, config).errorTypes - else -> emptySet() + + return event?.impl?.getErrorTypesFromStackframes() ?: ( + eventFile?.let { EventFilenameInfo.fromFile(it, config).errorTypes } + ?: emptySet() + ) + } + + private fun decodedEvent(): Event { + val localEvent = event + if (localEvent != null) { + return localEvent } + + val eventSource = MarshalledEventSource(eventFile!!, apiKey ?: config.apiKey, logger) + val decodedEvent = eventSource() + + // cache the decoded Event object + event = decodedEvent + + return decodedEvent + } + + /** + * If required trim this `EventPayload` so that its [encoded data](toByteArray) will usually be + * less-than or equal to [maxSizeBytes]. This function may make no changes to the payload, and + * may also not achieve the requested [maxSizeBytes]. The default use of the function is + * configured to [DEFAULT_MAX_PAYLOAD_SIZE]. + * + * @return `this` for call chaining + */ + @JvmOverloads + fun trimToSize(maxSizeBytes: Int = DEFAULT_MAX_PAYLOAD_SIZE): EventPayload { + var json = toByteArray() + if (json.size <= maxSizeBytes) { + return this + } + + val event = decodedEvent() + val (itemsTrimmed, dataTrimmed) = event.impl.trimMetadataStringsTo(config.maxStringValueLength) + event.impl.internalMetrics.setMetadataTrimMetrics( + itemsTrimmed, + dataTrimmed + ) + + json = rebuildPayloadCache() + if (json.size <= maxSizeBytes) { + return this + } + + val breadcrumbAndBytesRemovedCounts = + event.impl.trimBreadcrumbsBy(json.size - maxSizeBytes) + event.impl.internalMetrics.setBreadcrumbTrimMetrics( + breadcrumbAndBytesRemovedCounts.itemsTrimmed, + breadcrumbAndBytesRemovedCounts.dataTrimmed + ) + + rebuildPayloadCache() + return this } @Throws(IOException::class) @@ -51,4 +113,33 @@ class EventPayload @JvmOverloads internal constructor( writer.endArray() writer.endObject() } + + /** + * Transform this `EventPayload` to a byte array suitable for delivery to a BugSnag event + * endpoint (typically configured using [EndpointConfiguration.notify]). + */ + @Throws(IOException::class) + override fun toByteArray(): ByteArray { + var payload = cachedBytes + if (payload == null) { + payload = JsonHelper.serialize(this) + cachedBytes = payload + } + return payload + } + + @VisibleForTesting + internal fun rebuildPayloadCache(): ByteArray { + cachedBytes = null + return toByteArray() + } + + companion object { + /** + * The default maximum payload size for [trimToSize], payloads larger than this will + * typically be rejected by BugSnag. + */ + // 1MB with some fiddle room in case of encoding overhead + const val DEFAULT_MAX_PAYLOAD_SIZE = 999700 + } } diff --git a/bugsnag-android-core/src/main/java/com/bugsnag/android/EventStore.kt b/bugsnag-android-core/src/main/java/com/bugsnag/android/EventStore.kt index 5c3360dee9..75300b3907 100644 --- a/bugsnag-android-core/src/main/java/com/bugsnag/android/EventStore.kt +++ b/bugsnag-android-core/src/main/java/com/bugsnag/android/EventStore.kt @@ -32,7 +32,6 @@ internal class EventStore( ) : FileStore( File(config.persistenceDirectory.value, "bugsnag/errors"), config.maxPersistedEvents, - EVENT_COMPARATOR, logger, delegate ) { diff --git a/bugsnag-android-core/src/main/java/com/bugsnag/android/FileStore.kt b/bugsnag-android-core/src/main/java/com/bugsnag/android/FileStore.kt index 235ba699bf..34b5bb403e 100644 --- a/bugsnag-android-core/src/main/java/com/bugsnag/android/FileStore.kt +++ b/bugsnag-android-core/src/main/java/com/bugsnag/android/FileStore.kt @@ -7,8 +7,6 @@ import java.io.FileNotFoundException import java.io.FileOutputStream import java.io.OutputStreamWriter import java.io.Writer -import java.util.Collections -import java.util.Comparator import java.util.concurrent.ConcurrentSkipListSet import java.util.concurrent.locks.Lock import java.util.concurrent.locks.ReentrantLock @@ -16,7 +14,6 @@ import java.util.concurrent.locks.ReentrantLock internal abstract class FileStore( val storageDir: File, private val maxStoreCount: Int, - private val comparator: Comparator, protected open val logger: Logger, protected val delegate: Delegate? ) { @@ -115,23 +112,21 @@ internal abstract class FileStore( // Limit number of saved payloads to prevent disk space issues if (isStorageDirValid(storageDir)) { val listFiles = storageDir.listFiles() ?: return - val files: ArrayList = arrayListOf(*listFiles) - if (files.size >= maxStoreCount) { - // Sort files then delete the first one (oldest timestamp) - Collections.sort(files, comparator) - var k = 0 - while (k < files.size && files.size >= maxStoreCount) { - val oldestFile = files[k] - if (!queuedFiles.contains(oldestFile)) { - logger.w( - "Discarding oldest error as stored " + - "error limit reached: '" + oldestFile.path + '\'' - ) - deleteStoredFiles(setOf(oldestFile)) - files.removeAt(k) - k-- - } - k++ + if (listFiles.size < maxStoreCount) return + val sortedListFiles = listFiles.sortedBy { it.lastModified() } + // Number of files to discard takes into account that a new file may need to be written + val numberToDiscard = listFiles.size - maxStoreCount + 1 + var discardedCount = 0 + for (file in sortedListFiles) { + if (discardedCount == numberToDiscard) { + return + } else if (!queuedFiles.contains(file)) { + logger.w( + "Discarding oldest error as stored error limit reached: '" + + file.path + '\'' + ) + deleteStoredFiles(setOf(file)) + discardedCount++ } } } diff --git a/bugsnag-android-core/src/main/java/com/bugsnag/android/InternalReportDelegate.java b/bugsnag-android-core/src/main/java/com/bugsnag/android/InternalReportDelegate.java index a9d6a08d93..67bc393498 100644 --- a/bugsnag-android-core/src/main/java/com/bugsnag/android/InternalReportDelegate.java +++ b/bugsnag-android-core/src/main/java/com/bugsnag/android/InternalReportDelegate.java @@ -5,7 +5,6 @@ import com.bugsnag.android.internal.BackgroundTaskService; import com.bugsnag.android.internal.ImmutableConfig; -import com.bugsnag.android.internal.JsonHelper; import com.bugsnag.android.internal.TaskType; import android.annotation.SuppressLint; @@ -126,7 +125,8 @@ public void run() { DefaultDelivery defaultDelivery = (DefaultDelivery) delivery; defaultDelivery.deliver( params.getEndpoint(), - JsonHelper.INSTANCE.serialize(payload), + payload.toByteArray(), + payload.getIntegrityToken(), headers ); } diff --git a/bugsnag-android-core/src/main/java/com/bugsnag/android/JsonStream.java b/bugsnag-android-core/src/main/java/com/bugsnag/android/JsonStream.java index 57072f22b6..a32413457c 100644 --- a/bugsnag-android-core/src/main/java/com/bugsnag/android/JsonStream.java +++ b/bugsnag-android-core/src/main/java/com/bugsnag/android/JsonStream.java @@ -64,7 +64,11 @@ public void value(@Nullable Object object, boolean shouldRedactKeys) throws IOEx * Collections, Maps, and arrays. */ public void value(@Nullable Object object) throws IOException { - value(object, false); + if (object instanceof File) { + value((File) object); + } else { + value(object, false); + } } /** diff --git a/bugsnag-android-core/src/main/java/com/bugsnag/android/Notifier.kt b/bugsnag-android-core/src/main/java/com/bugsnag/android/Notifier.kt index 9072654367..117d0da716 100644 --- a/bugsnag-android-core/src/main/java/com/bugsnag/android/Notifier.kt +++ b/bugsnag-android-core/src/main/java/com/bugsnag/android/Notifier.kt @@ -7,7 +7,7 @@ import java.io.IOException */ class Notifier @JvmOverloads constructor( var name: String = "Android Bugsnag Notifier", - var version: String = "6.8.0", + var version: String = "6.9.0", var url: String = "https://bugsnag.com" ) : JsonStream.Streamable { diff --git a/bugsnag-android-core/src/main/java/com/bugsnag/android/Session.java b/bugsnag-android-core/src/main/java/com/bugsnag/android/Session.java index 5081ad22e7..f838aa37e2 100644 --- a/bugsnag-android-core/src/main/java/com/bugsnag/android/Session.java +++ b/bugsnag-android-core/src/main/java/com/bugsnag/android/Session.java @@ -1,6 +1,7 @@ package com.bugsnag.android; import com.bugsnag.android.internal.DateUtils; +import com.bugsnag.android.internal.JsonHelper; import androidx.annotation.NonNull; import androidx.annotation.Nullable; @@ -17,7 +18,7 @@ * Represents a contiguous session in an application. */ @SuppressWarnings("ConstantConditions") -public final class Session implements JsonStream.Streamable, UserAware { +public final class Session implements JsonStream.Streamable, Deliverable, UserAware { private final File file; private final Notifier notifier; @@ -258,6 +259,17 @@ public void toStream(@NonNull JsonStream writer) throws IOException { } } + @NonNull + public byte[] toByteArray() throws IOException { + return JsonHelper.INSTANCE.serialize(this); + } + + @Nullable + @Override + public String getIntegrityToken() { + return Deliverable.DefaultImpls.getIntegrityToken(this); + } + private void serializePayload(@NonNull JsonStream writer) throws IOException { writer.value(file); } diff --git a/bugsnag-android-core/src/main/java/com/bugsnag/android/SessionStore.kt b/bugsnag-android-core/src/main/java/com/bugsnag/android/SessionStore.kt index 86519ac5af..094de50547 100644 --- a/bugsnag-android-core/src/main/java/com/bugsnag/android/SessionStore.kt +++ b/bugsnag-android-core/src/main/java/com/bugsnag/android/SessionStore.kt @@ -21,7 +21,6 @@ internal class SessionStore( config.persistenceDirectory.value, "bugsnag/sessions" ), config.maxPersistedSessions, - SESSION_COMPARATOR, logger, delegate ) { diff --git a/bugsnag-android-core/src/main/java/com/bugsnag/android/internal/ImmutableConfig.kt b/bugsnag-android-core/src/main/java/com/bugsnag/android/internal/ImmutableConfig.kt index 17e195727b..42e9c607ce 100644 --- a/bugsnag-android-core/src/main/java/com/bugsnag/android/internal/ImmutableConfig.kt +++ b/bugsnag-android-core/src/main/java/com/bugsnag/android/internal/ImmutableConfig.kt @@ -53,6 +53,7 @@ data class ImmutableConfig( val maxPersistedEvents: Int, val maxPersistedSessions: Int, val maxReportedThreads: Int, + val maxStringValueLength: Int, val threadCollectionTimeLimitMillis: Long, val persistenceDirectory: Lazy, val sendLaunchCrashesSynchronously: Boolean, @@ -174,6 +175,7 @@ internal fun convertToImmutableConfig( maxPersistedEvents = config.maxPersistedEvents, maxPersistedSessions = config.maxPersistedSessions, maxReportedThreads = config.maxReportedThreads, + maxStringValueLength = config.maxStringValueLength, threadCollectionTimeLimitMillis = config.threadCollectionTimeLimitMillis, enabledBreadcrumbTypes = config.enabledBreadcrumbTypes?.toSet(), telemetry = config.telemetry.toSet(), @@ -256,12 +258,7 @@ internal fun sanitiseConfiguration( @Suppress("SENSELESS_COMPARISON") if (configuration.delivery == null) { - configuration.delivery = DefaultDelivery( - connectivity, - configuration.apiKey, - configuration.maxStringValueLength, - configuration.logger!! - ) + configuration.delivery = DefaultDelivery(connectivity, configuration.logger!!) } return convertToImmutableConfig( configuration, diff --git a/bugsnag-android-core/src/test/java/com/bugsnag/android/DeliveryHeadersTest.kt b/bugsnag-android-core/src/test/java/com/bugsnag/android/DeliveryHeadersTest.kt index 0ce163179a..500451261e 100644 --- a/bugsnag-android-core/src/test/java/com/bugsnag/android/DeliveryHeadersTest.kt +++ b/bugsnag-android-core/src/test/java/com/bugsnag/android/DeliveryHeadersTest.kt @@ -3,7 +3,6 @@ package com.bugsnag.android import com.bugsnag.android.BugsnagTestUtils.generateConfiguration import com.bugsnag.android.BugsnagTestUtils.generateEventPayload import com.bugsnag.android.BugsnagTestUtils.generateImmutableConfig -import com.bugsnag.android.internal.JsonHelper import com.bugsnag.android.internal.convertToImmutableConfig import org.junit.Assert.assertEquals import org.junit.Assert.assertNotEquals @@ -26,10 +25,8 @@ class DeliveryHeadersTest { @Test fun computeSha1Digest() { val payload = generateEventPayload(generateImmutableConfig()) - val payload1 = JsonHelper.serialize(payload) - val firstSha = requireNotNull(computeSha1Digest(payload1)) - val payload2 = JsonHelper.serialize(payload) - val secondSha = requireNotNull(computeSha1Digest(payload2)) + val firstSha = requireNotNull(payload.integrityToken) + val secondSha = requireNotNull(payload.integrityToken) // the hash equals the expected value assertTrue(firstSha.matches(sha1Regex)) @@ -39,8 +36,8 @@ class DeliveryHeadersTest { // altering the streamable alters the hash payload.event!!.device.id = "50923" - val payload3 = JsonHelper.serialize(payload) - val differentSha = requireNotNull(computeSha1Digest(payload3)) + payload.rebuildPayloadCache() + val differentSha = requireNotNull(payload.integrityToken) assertNotEquals(firstSha, differentSha) assertTrue(differentSha.matches(sha1Regex)) } diff --git a/bugsnag-android-core/src/test/java/com/bugsnag/android/DeliveryStatusTest.kt b/bugsnag-android-core/src/test/java/com/bugsnag/android/DeliveryStatusTest.kt new file mode 100644 index 0000000000..5ca0b908d8 --- /dev/null +++ b/bugsnag-android-core/src/test/java/com/bugsnag/android/DeliveryStatusTest.kt @@ -0,0 +1,22 @@ +package com.bugsnag.android + +import org.junit.Assert.assertEquals +import org.junit.Test + +class DeliveryStatusTest { + + @Test + fun testResponseCodeMapping() { + assertEquals(DeliveryStatus.DELIVERED, DeliveryStatus.forHttpResponseCode(202)) + assertEquals(DeliveryStatus.UNDELIVERED, DeliveryStatus.forHttpResponseCode(503)) + assertEquals(DeliveryStatus.UNDELIVERED, DeliveryStatus.forHttpResponseCode(0)) + assertEquals(DeliveryStatus.UNDELIVERED, DeliveryStatus.forHttpResponseCode(408)) + assertEquals(DeliveryStatus.UNDELIVERED, DeliveryStatus.forHttpResponseCode(429)) + assertEquals(DeliveryStatus.FAILURE, DeliveryStatus.forHttpResponseCode(400)) + assertEquals(DeliveryStatus.FAILURE, DeliveryStatus.forHttpResponseCode(401)) + assertEquals(DeliveryStatus.FAILURE, DeliveryStatus.forHttpResponseCode(498)) + assertEquals(DeliveryStatus.FAILURE, DeliveryStatus.forHttpResponseCode(499)) + assertEquals(DeliveryStatus.UNDELIVERED, DeliveryStatus.forHttpResponseCode(408)) + assertEquals(DeliveryStatus.UNDELIVERED, DeliveryStatus.forHttpResponseCode(429)) + } +} diff --git a/bugsnag-android-core/src/test/java/com/bugsnag/android/DeliveryTest.kt b/bugsnag-android-core/src/test/java/com/bugsnag/android/DeliveryTest.kt deleted file mode 100644 index b39dfb1364..0000000000 --- a/bugsnag-android-core/src/test/java/com/bugsnag/android/DeliveryTest.kt +++ /dev/null @@ -1,23 +0,0 @@ -package com.bugsnag.android - -import org.junit.Assert.assertEquals -import org.junit.Test - -class DeliveryTest { - - @Test - fun testResponseCodeMapping() { - val delivery = DefaultDelivery(null, "myApiKey", 10000, NoopLogger) - assertEquals(DeliveryStatus.DELIVERED, delivery.getDeliveryStatus(202)) - assertEquals(DeliveryStatus.UNDELIVERED, delivery.getDeliveryStatus(503)) - assertEquals(DeliveryStatus.UNDELIVERED, delivery.getDeliveryStatus(0)) - assertEquals(DeliveryStatus.UNDELIVERED, delivery.getDeliveryStatus(408)) - assertEquals(DeliveryStatus.UNDELIVERED, delivery.getDeliveryStatus(429)) - assertEquals(DeliveryStatus.FAILURE, delivery.getDeliveryStatus(400)) - assertEquals(DeliveryStatus.FAILURE, delivery.getDeliveryStatus(401)) - assertEquals(DeliveryStatus.FAILURE, delivery.getDeliveryStatus(498)) - assertEquals(DeliveryStatus.FAILURE, delivery.getDeliveryStatus(499)) - assertEquals(DeliveryStatus.UNDELIVERED, delivery.getDeliveryStatus(408)) - assertEquals(DeliveryStatus.UNDELIVERED, delivery.getDeliveryStatus(429)) - } -} diff --git a/bugsnag-plugin-android-ndk/src/main/jni/event.h b/bugsnag-plugin-android-ndk/src/main/jni/event.h index d92324f394..4a63c721dc 100644 --- a/bugsnag-plugin-android-ndk/src/main/jni/event.h +++ b/bugsnag-plugin-android-ndk/src/main/jni/event.h @@ -191,6 +191,7 @@ typedef struct { typedef struct { pid_t id; + bool is_reporting_thread; char name[16]; char state[13]; } bsg_thread; diff --git a/bugsnag-plugin-android-ndk/src/main/jni/handlers/cpp_handler.cpp b/bugsnag-plugin-android-ndk/src/main/jni/handlers/cpp_handler.cpp index af8d7bf0f3..c02e8371bf 100644 --- a/bugsnag-plugin-android-ndk/src/main/jni/handlers/cpp_handler.cpp +++ b/bugsnag-plugin-android-ndk/src/main/jni/handlers/cpp_handler.cpp @@ -4,6 +4,7 @@ #include #include #include +#include #include "../utils/crash_info.h" #include "../utils/serializer.h" @@ -58,7 +59,7 @@ void bsg_handle_cpp_terminate() { if (bsg_global_env->send_threads != SEND_THREADS_NEVER) { bsg_global_env->next_event.thread_count = bsg_capture_thread_states( - bsg_global_env->next_event.threads, BUGSNAG_THREADS_MAX); + gettid(), bsg_global_env->next_event.threads, BUGSNAG_THREADS_MAX); } else { bsg_global_env->next_event.thread_count = 0; } diff --git a/bugsnag-plugin-android-ndk/src/main/jni/handlers/signal_handler.c b/bugsnag-plugin-android-ndk/src/main/jni/handlers/signal_handler.c index 15afe15f70..613f747af8 100644 --- a/bugsnag-plugin-android-ndk/src/main/jni/handlers/signal_handler.c +++ b/bugsnag-plugin-android-ndk/src/main/jni/handlers/signal_handler.c @@ -188,7 +188,7 @@ void bsg_handle_signal(int signum, siginfo_t *info, if (bsg_global_env->send_threads != SEND_THREADS_NEVER) { bsg_global_env->next_event.thread_count = bsg_capture_thread_states( - bsg_global_env->next_event.threads, BUGSNAG_THREADS_MAX); + gettid(), bsg_global_env->next_event.threads, BUGSNAG_THREADS_MAX); } else { bsg_global_env->next_event.thread_count = 0; } diff --git a/bugsnag-plugin-android-ndk/src/main/jni/utils/serializer/event_writer.c b/bugsnag-plugin-android-ndk/src/main/jni/utils/serializer/event_writer.c index 935890205d..4b687eb7c8 100644 --- a/bugsnag-plugin-android-ndk/src/main/jni/utils/serializer/event_writer.c +++ b/bugsnag-plugin-android-ndk/src/main/jni/utils/serializer/event_writer.c @@ -605,6 +605,12 @@ static bool bsg_write_threads(BSG_KSJSONEncodeContext *json, CHECKED(bsg_ksjsonbeginObject(json, NULL)); { CHECKED(JSON_LIMITED_STRING_ELEMENT("id", id_string)); + + if (thread->is_reporting_thread) { + CHECKED( + bsg_ksjsonaddBooleanElement(json, "errorReportingThread", true)); + } + CHECKED(JSON_LIMITED_STRING_ELEMENT("name", thread->name)); CHECKED(JSON_LIMITED_STRING_ELEMENT("state", thread->state)); CHECKED(JSON_CONSTANT_ELEMENT("type", "c")); diff --git a/bugsnag-plugin-android-ndk/src/main/jni/utils/threads.c b/bugsnag-plugin-android-ndk/src/main/jni/utils/threads.c index 289477fddc..e641230208 100644 --- a/bugsnag-plugin-android-ndk/src/main/jni/utils/threads.c +++ b/bugsnag-plugin-android-ndk/src/main/jni/utils/threads.c @@ -167,7 +167,8 @@ static bool read_thread_state(bsg_thread *dest, const char *tid) { return parse_success; } -size_t bsg_capture_thread_states(bsg_thread *threads, size_t max_threads) { +size_t bsg_capture_thread_states(pid_t reporting_tid, bsg_thread *threads, + size_t max_threads) { size_t total_thread_count = 0; struct dirent64 *entry; char buffer[1024]; @@ -187,6 +188,8 @@ size_t bsg_capture_thread_states(bsg_thread *threads, size_t max_threads) { for (offset = 0; offset < available && total_thread_count < max_threads;) { entry = (struct dirent64 *)(buffer + offset); if (read_thread_state(&threads[total_thread_count], entry->d_name)) { + threads[total_thread_count].is_reporting_thread = + threads[total_thread_count].id == reporting_tid; total_thread_count += 1; } diff --git a/bugsnag-plugin-android-ndk/src/main/jni/utils/threads.h b/bugsnag-plugin-android-ndk/src/main/jni/utils/threads.h index b3943adcd6..df386dd6fc 100644 --- a/bugsnag-plugin-android-ndk/src/main/jni/utils/threads.h +++ b/bugsnag-plugin-android-ndk/src/main/jni/utils/threads.h @@ -10,7 +10,7 @@ extern "C" { #define MAX_STAT_PATH_LENGTH 64 -size_t bsg_capture_thread_states(bsg_thread *threads, +size_t bsg_capture_thread_states(pid_t reporting_tid, bsg_thread *threads, size_t max_threads) __asyncsafe; #ifdef __cplusplus diff --git a/bugsnag-plugin-android-okhttp/api/bugsnag-plugin-android-okhttp.api b/bugsnag-plugin-android-okhttp/api/bugsnag-plugin-android-okhttp.api index f133371aa4..3301f0d212 100644 --- a/bugsnag-plugin-android-okhttp/api/bugsnag-plugin-android-okhttp.api +++ b/bugsnag-plugin-android-okhttp/api/bugsnag-plugin-android-okhttp.api @@ -13,3 +13,12 @@ public final class com/bugsnag/android/okhttp/BugsnagOkHttpPlugin : okhttp3/Even public fun unload ()V } +public final class com/bugsnag/android/okhttp/OkHttpDelivery : com/bugsnag/android/Delivery { + public fun ()V + public fun (Lokhttp3/OkHttpClient;)V + public fun (Lokhttp3/OkHttpClient;Lcom/bugsnag/android/Logger;)V + public synthetic fun (Lokhttp3/OkHttpClient;Lcom/bugsnag/android/Logger;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public fun deliver (Lcom/bugsnag/android/EventPayload;Lcom/bugsnag/android/DeliveryParams;)Lcom/bugsnag/android/DeliveryStatus; + public fun deliver (Lcom/bugsnag/android/Session;Lcom/bugsnag/android/DeliveryParams;)Lcom/bugsnag/android/DeliveryStatus; +} + diff --git a/bugsnag-plugin-android-okhttp/src/main/java/com/bugsnag/android/okhttp/OkHttpDelivery.kt b/bugsnag-plugin-android-okhttp/src/main/java/com/bugsnag/android/okhttp/OkHttpDelivery.kt new file mode 100644 index 0000000000..6f5f230dce --- /dev/null +++ b/bugsnag-plugin-android-okhttp/src/main/java/com/bugsnag/android/okhttp/OkHttpDelivery.kt @@ -0,0 +1,91 @@ +package com.bugsnag.android.okhttp + +import android.net.TrafficStats +import com.bugsnag.android.Delivery +import com.bugsnag.android.DeliveryParams +import com.bugsnag.android.DeliveryStatus +import com.bugsnag.android.EventPayload +import com.bugsnag.android.Logger +import com.bugsnag.android.Session +import okhttp3.Headers +import okhttp3.OkHttpClient +import okhttp3.Request +import okhttp3.RequestBody.Companion.toRequestBody +import java.io.IOException + +private const val BUGSNAG_INTEGRITY_HEADER = "Bugsnag-Integrity" + +class OkHttpDelivery @JvmOverloads constructor( + private val client: OkHttpClient = OkHttpClient.Builder().build(), + private val logger: Logger? = null, +) : Delivery { + override fun deliver(payload: Session, deliveryParams: DeliveryParams): DeliveryStatus { + TrafficStats.setThreadStatsTag(1) + try { + val requestBody = payload.toByteArray().toRequestBody() + val integrityHeader = payload.integrityToken + + val requestBuilder = Request.Builder() + .url(deliveryParams.endpoint) + .headers(deliveryParams.toHeaders()) + .post(requestBody) + + if (integrityHeader != null) { + requestBuilder.header(BUGSNAG_INTEGRITY_HEADER, integrityHeader) + } + + val call = client.newCall(requestBuilder.build()) + val response = call.execute() + + return DeliveryStatus.forHttpResponseCode(response.code) + } finally { + TrafficStats.clearThreadStatsTag() + } + } + + override fun deliver(payload: EventPayload, deliveryParams: DeliveryParams): DeliveryStatus { + TrafficStats.setThreadStatsTag(1) + try { + val requestBody = payload.trimToSize().toByteArray().toRequestBody() + val integrityHeader = payload.integrityToken + + val requestBuilder = Request.Builder() + .url(deliveryParams.endpoint) + .headers(deliveryParams.toHeaders()) + .post(requestBody) + + if (integrityHeader != null) { + requestBuilder.header(BUGSNAG_INTEGRITY_HEADER, integrityHeader) + } + + val call = client.newCall(requestBuilder.build()) + + val response = call.execute() + return DeliveryStatus.forHttpResponseCode(response.code) + } catch (oom: OutOfMemoryError) { + // attempt to persist the payload on disk. This approach uses streams to write to a + // file, which takes less memory than serializing the payload into a ByteArray, and + // therefore has a reasonable chance of retaining the payload for future delivery. + logger?.w("Encountered OOM delivering payload, falling back to persist on disk", oom) + return DeliveryStatus.UNDELIVERED + } catch (exception: IOException) { + logger?.w("IOException encountered in request", exception) + return DeliveryStatus.UNDELIVERED + } catch (exception: Exception) { + logger?.w("Unexpected error delivering payload", exception) + return DeliveryStatus.FAILURE + } finally { + TrafficStats.clearThreadStatsTag() + } + } + + private fun DeliveryParams.toHeaders(): Headers { + return Headers.Builder().run { + headers.forEach { (name, value) -> + value?.let { add(name, it) } + } + + build() + } + } +} diff --git a/bugsnag-plugin-react-native/src/test/java/com/bugsnag/android/TestData.java b/bugsnag-plugin-react-native/src/test/java/com/bugsnag/android/TestData.java index b08eae88d2..85e66461e7 100644 --- a/bugsnag-plugin-react-native/src/test/java/com/bugsnag/android/TestData.java +++ b/bugsnag-plugin-react-native/src/test/java/com/bugsnag/android/TestData.java @@ -32,7 +32,7 @@ static ImmutableConfig generateConfig() throws IOException { "1.4.3", 55, "android", - new DefaultDelivery(null, "myApiKey", 10000, NoopLogger.INSTANCE), + new DefaultDelivery(null, NoopLogger.INSTANCE), new EndpointConfiguration(), true, 55, @@ -41,6 +41,7 @@ static ImmutableConfig generateConfig() throws IOException { 32, 32, 1000, + 10000, 500, LazyKt.lazy(new Function0() { @Override diff --git a/examples/sdk-app-example/app/build.gradle b/examples/sdk-app-example/app/build.gradle index fa240090ea..7b423075f9 100644 --- a/examples/sdk-app-example/app/build.gradle +++ b/examples/sdk-app-example/app/build.gradle @@ -42,8 +42,8 @@ android { } dependencies { - implementation "com.bugsnag:bugsnag-android:6.8.0" - implementation "com.bugsnag:bugsnag-plugin-android-okhttp:6.8.0" + implementation "com.bugsnag:bugsnag-android:6.9.0" + implementation "com.bugsnag:bugsnag-plugin-android-okhttp:6.9.0" implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" implementation "androidx.appcompat:appcompat:1.6.1" implementation "com.google.android.material:material:1.11.0" diff --git a/features/fixtures/mazerunner/app/src/main/java/com/bugsnag/android/mazerunner/MazerunnerApp.kt b/features/fixtures/mazerunner/app/src/main/java/com/bugsnag/android/mazerunner/MazerunnerApp.kt index f07e7fd46c..1f305eee0e 100644 --- a/features/fixtures/mazerunner/app/src/main/java/com/bugsnag/android/mazerunner/MazerunnerApp.kt +++ b/features/fixtures/mazerunner/app/src/main/java/com/bugsnag/android/mazerunner/MazerunnerApp.kt @@ -30,6 +30,7 @@ class MazerunnerApp : Application() { val policy = StrictMode.VmPolicy.Builder() .detectNonSdkApiUsage() .penaltyDeath() + .penaltyLog() .build() StrictMode.setVmPolicy(policy) } diff --git a/features/fixtures/mazerunner/jvm-scenarios/detekt-baseline.xml b/features/fixtures/mazerunner/jvm-scenarios/detekt-baseline.xml index cbe2ac2438..73a9ae16a7 100644 --- a/features/fixtures/mazerunner/jvm-scenarios/detekt-baseline.xml +++ b/features/fixtures/mazerunner/jvm-scenarios/detekt-baseline.xml @@ -26,6 +26,7 @@ MagicNumber:TrimmedStacktraceScenario.kt$TrimmedStacktraceScenario$100000 MagicNumber:UnhandledExceptionEventDetailChangeScenario.kt$UnhandledExceptionEventDetailChangeScenario$123 MagicNumber:UnhandledExceptionEventDetailChangeScenario.kt$UnhandledExceptionEventDetailChangeScenario$123456 + MaxLineLength:OkHttpDeliveryScenario.kt$OkHttpDeliveryScenario$// StrictMode policy violation: android.os.strictmode.NonSdkApiUsedViolation: Lcom/android/org/conscrypt/OpenSSLSocketImpl;->setUseSessionTickets(Z)V ThrowingExceptionsWithoutMessageOrCause:AnrHelper.kt$<no name provided>$IllegalStateException() ThrowingExceptionsWithoutMessageOrCause:BugsnagInitScenario.kt$BugsnagInitScenario$RuntimeException() ThrowingExceptionsWithoutMessageOrCause:CustomPluginNotifierDescriptionScenario.kt$CustomPluginNotifierDescriptionScenario$RuntimeException() @@ -36,6 +37,7 @@ TooGenericExceptionThrown:DeliverOnCrashScenario.kt$DeliverOnCrashScenario$throw RuntimeException("DeliverOnCrashScenario") TooGenericExceptionThrown:DisableAutoDetectErrorsScenario.kt$DisableAutoDetectErrorsScenario$throw RuntimeException("Should never appear") TooGenericExceptionThrown:FeatureFlagScenario.kt$FeatureFlagScenario$throw RuntimeException("FeatureFlagScenario unhandled") + TooGenericExceptionThrown:OkHttpDeliveryScenario.kt$OkHttpDeliveryScenario$throw RuntimeException("Unhandled Error") TooGenericExceptionThrown:OnSendCallbackScenario.kt$OnSendCallbackScenario$throw RuntimeException("Unhandled Error") TooGenericExceptionThrown:ReportCacheScenario.kt$ReportCacheScenario$throw RuntimeException("ReportCacheScenario") TooGenericExceptionThrown:StartupCrashFlushScenario.kt$StartupCrashFlushScenario$throw RuntimeException("Regular crash") diff --git a/features/fixtures/mazerunner/jvm-scenarios/src/main/java/com/bugsnag/android/JavaHooks.java b/features/fixtures/mazerunner/jvm-scenarios/src/main/java/com/bugsnag/android/JavaHooks.java index 84a2040477..9d636da96a 100644 --- a/features/fixtures/mazerunner/jvm-scenarios/src/main/java/com/bugsnag/android/JavaHooks.java +++ b/features/fixtures/mazerunner/jvm-scenarios/src/main/java/com/bugsnag/android/JavaHooks.java @@ -56,7 +56,7 @@ public DeliveryStatus deliver(@NonNull Session payload, @NonNull public static Delivery createDefaultDelivery() { - return new DefaultDelivery(null, "test-api-key", 10000, NoopLogger.INSTANCE); + return new DefaultDelivery(null, NoopLogger.INSTANCE); } /** diff --git a/features/fixtures/mazerunner/jvm-scenarios/src/main/java/com/bugsnag/android/mazerunner/scenarios/ManualSessionSmokeScenario.kt b/features/fixtures/mazerunner/jvm-scenarios/src/main/java/com/bugsnag/android/mazerunner/scenarios/ManualSessionSmokeScenario.kt index b9a50c3a61..c552d721b7 100644 --- a/features/fixtures/mazerunner/jvm-scenarios/src/main/java/com/bugsnag/android/mazerunner/scenarios/ManualSessionSmokeScenario.kt +++ b/features/fixtures/mazerunner/jvm-scenarios/src/main/java/com/bugsnag/android/mazerunner/scenarios/ManualSessionSmokeScenario.kt @@ -6,6 +6,7 @@ import com.bugsnag.android.Configuration import com.bugsnag.android.createDefaultDelivery import com.bugsnag.android.mazerunner.InterceptingDelivery import java.util.concurrent.CountDownLatch +import kotlin.concurrent.thread /** * Sends an exception after pausing the session @@ -27,23 +28,25 @@ internal class ManualSessionSmokeScenario( override fun startScenario() { super.startScenario() - Bugsnag.setUser("123", "ABC.CBA.CA", "ManualSessionSmokeScenario") + thread { + Bugsnag.setUser("123", "ABC.CBA.CA", "ManualSessionSmokeScenario") - // send session - Bugsnag.startSession() + // send session + Bugsnag.startSession() - // send exception with session - Bugsnag.notify(generateException()) + // send exception with session + Bugsnag.notify(generateException()) - // send exception without session - Bugsnag.pauseSession() - Bugsnag.notify(generateException()) + // send exception without session + Bugsnag.pauseSession() + Bugsnag.notify(generateException()) - // override to ensure request order, as the order of fatal errors - // can be indeterminate if they are persisted to disk at the same - // millisecond as another error. - deliveryLatch.await() - Bugsnag.resumeSession() - throw generateException() + // override to ensure request order, as the order of fatal errors + // can be indeterminate if they are persisted to disk at the same + // millisecond as another error. + deliveryLatch.await() + Bugsnag.resumeSession() + throw generateException() + } } } diff --git a/features/fixtures/mazerunner/jvm-scenarios/src/main/java/com/bugsnag/android/mazerunner/scenarios/OkHttpDeliveryScenario.kt b/features/fixtures/mazerunner/jvm-scenarios/src/main/java/com/bugsnag/android/mazerunner/scenarios/OkHttpDeliveryScenario.kt new file mode 100644 index 0000000000..e26e31a794 --- /dev/null +++ b/features/fixtures/mazerunner/jvm-scenarios/src/main/java/com/bugsnag/android/mazerunner/scenarios/OkHttpDeliveryScenario.kt @@ -0,0 +1,29 @@ +package com.bugsnag.android.mazerunner.scenarios + +import android.content.Context +import android.os.StrictMode +import com.bugsnag.android.Configuration +import com.bugsnag.android.okhttp.OkHttpDelivery +import java.lang.RuntimeException + +internal class OkHttpDeliveryScenario( + config: Configuration, + context: Context, + eventMetadata: String +) : Scenario(config, context, eventMetadata) { + + init { + // there is a StrictMode violation within the OkHttp version we use, so we turn off + // StrictMode for this scenario + // StrictMode policy violation: android.os.strictmode.NonSdkApiUsedViolation: Lcom/android/org/conscrypt/OpenSSLSocketImpl;->setUseSessionTickets(Z)V + StrictMode.setThreadPolicy(StrictMode.ThreadPolicy.LAX) + StrictMode.setVmPolicy(StrictMode.VmPolicy.LAX) + + config.delivery = OkHttpDelivery() + } + + override fun startScenario() { + super.startScenario() + throw RuntimeException("Unhandled Error") + } +} diff --git a/features/full_tests/crash_handler.feature b/features/full_tests/crash_handler.feature index 98b6ff4e8b..975c2a1672 100644 --- a/features/full_tests/crash_handler.feature +++ b/features/full_tests/crash_handler.feature @@ -21,3 +21,12 @@ Feature: Reporting with other exception handlers installed And the exception "errorClass" equals "java.lang.RuntimeException" And the exception "message" equals "DeliverOnCrashScenario" And the event "usage.config.attemptDeliveryOnCrash" is true + + Scenario: OkHttpDelivery is used to deliver payloads + When I run "OkHttpDeliveryScenario" and relaunch the crashed app + And I configure Bugsnag for "OkHttpDeliveryScenario" + And I wait to receive an error + Then the error is valid for the error reporting API version "4.0" for the "Android Bugsnag Notifier" notifier + And the error payload field "events" is an array with 1 elements + And the exception "errorClass" equals "java.lang.RuntimeException" + And the exception "message" equals "Unhandled Error" diff --git a/features/smoke_tests/04_unhandled.feature b/features/smoke_tests/04_unhandled.feature index 6eb54cffb8..73158a07c4 100644 --- a/features/smoke_tests/04_unhandled.feature +++ b/features/smoke_tests/04_unhandled.feature @@ -292,3 +292,9 @@ Feature: Unhandled smoke tests # Breadcrumbs And the event has a "manual" breadcrumb named "CXXExceptionSmokeScenario" + # Threads validation + And the error payload field "events.0.threads" is a non-empty array + And the event "threads.0.id" matches "^[0-9]+$" + And the event "threads.0.name" is not null + And the event "threads.0.type" equals "c" + And the thread with name "roid.mazerunner" contains the error reporting flag \ No newline at end of file diff --git a/gradle.properties b/gradle.properties index 6e11b5b1ba..5c5ba7faea 100644 --- a/gradle.properties +++ b/gradle.properties @@ -11,7 +11,7 @@ org.gradle.jvmargs=-Xmx4096m # This option should only be used with decoupled projects. More details, visit # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects org.gradle.parallel=true -VERSION_NAME=6.8.0 +VERSION_NAME=6.9.0 GROUP=com.bugsnag POM_SCM_URL=https://github.com/bugsnag/bugsnag-android POM_SCM_CONNECTION=scm:git@github.com:bugsnag/bugsnag-android.git