From 3ef6dc352edad4a0267e30e5dc8420281376cfbb Mon Sep 17 00:00:00 2001 From: hannah-smartbear Date: Tue, 17 Sep 2024 10:14:09 +0100 Subject: [PATCH 01/15] Simplifying discardOldestFileIfNeeded function --- .../java/com/bugsnag/android/FileStoreTest.kt | 7 ++-- .../java/com/bugsnag/android/EventStore.kt | 1 - .../java/com/bugsnag/android/FileStore.kt | 35 ++++++++----------- .../java/com/bugsnag/android/SessionStore.kt | 1 - 4 files changed, 17 insertions(+), 27 deletions(-) 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/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/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 ) { From d54e6fb45f4e8f0126ee295a83ba95cfb02b87a5 Mon Sep 17 00:00:00 2001 From: jason Date: Fri, 4 Oct 2024 09:04:47 +0100 Subject: [PATCH 02/15] feat(ndk): identify the error reporting thread in native crashes --- CHANGELOG.md | 7 +++++++ bugsnag-plugin-android-ndk/src/main/jni/event.h | 1 + .../src/main/jni/handlers/cpp_handler.cpp | 3 ++- .../src/main/jni/handlers/signal_handler.c | 2 +- .../src/main/jni/utils/serializer/event_writer.c | 6 ++++++ bugsnag-plugin-android-ndk/src/main/jni/utils/threads.c | 5 ++++- bugsnag-plugin-android-ndk/src/main/jni/utils/threads.h | 2 +- features/smoke_tests/04_unhandled.feature | 6 ++++++ 8 files changed, 28 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f140ed0104..f6856071c4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,12 @@ # Changelog +## TBD + +### Enhancements + +* Native crashes will now identify the crashing/error reporting thread + [#2087](https://github.com/bugsnag/bugsnag-android/pull/2087) + ## 6.8.0 (2024-09-30) ### Enhancements 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/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 From 08fb1b5715fef4343d5bf5eeefab029cfc357eda Mon Sep 17 00:00:00 2001 From: jason Date: Tue, 29 Oct 2024 11:29:27 +0000 Subject: [PATCH 03/15] test(ManualSessionSmokeScenario): moved the test onto a background thread to try and avoid ANRs --- .../scenarios/ManualSessionSmokeScenario.kt | 31 ++++++++++--------- 1 file changed, 17 insertions(+), 14 deletions(-) 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() + } } } From 97eeab611ee2c0181e6de0904c7ce385bc2b83bb Mon Sep 17 00:00:00 2001 From: jason Date: Tue, 22 Oct 2024 17:05:07 +0100 Subject: [PATCH 04/15] refactor(delivery): moved much of the delivery mechanics into EventPayload, DeliveryStatus, and Session to allow easier development of new `Delivery` implementations --- .../com/bugsnag/android/DefaultDelivery.kt | 70 ++--------- .../com/bugsnag/android/DeliveryHeaders.kt | 18 --- .../com/bugsnag/android/DeliveryStatus.kt | 21 +++- .../java/com/bugsnag/android/EventPayload.kt | 112 ++++++++++++++++-- .../android/InternalReportDelegate.java | 12 +- .../java/com/bugsnag/android/Session.java | 9 +- .../android/internal/ImmutableConfig.kt | 9 +- .../bugsnag/android/okhttp/OkHttpDelivery.kt | 78 ++++++++++++ .../java/com/bugsnag/android/TestData.java | 1 + 9 files changed, 228 insertions(+), 102 deletions(-) create mode 100644 bugsnag-plugin-android-okhttp/src/main/java/com/bugsnag/android/okhttp/OkHttpDelivery.kt 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..d6be8503eb 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), + null, 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.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/DeliveryHeaders.kt b/bugsnag-android-core/src/main/java/com/bugsnag/android/DeliveryHeaders.kt index 89df18055d..9620699e55 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 @@ -60,24 +60,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..a2a168fa3f 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,20 @@ enum class DeliveryStatus { * * The payload was not delivered and should be deleted without attempting retry. */ - FAILURE + FAILURE; + + companion object { + fun forHttpResponseCode(responseCode: Int): DeliveryStatus { + return when { + responseCode in HTTP_OK..299 -> DELIVERED + isUnrecoverableStatusCode(responseCode) -> FAILURE + else -> 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/EventPayload.kt b/bugsnag-android-core/src/main/java/com/bugsnag/android/EventPayload.kt index 51f213016d..fa183dbceb 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,8 +1,12 @@ 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 +import java.security.DigestOutputStream +import java.security.MessageDigest /** * An error report payload. @@ -13,13 +17,21 @@ 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 { - var event = event - internal set(value) { field = value } + @VisibleForTesting + internal var event: Event? = event + private set + + internal var eventFile: File? = eventFile + private set + + private var cachedPayloadBytes: 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 +39,59 @@ 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()) + } + + internal 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 + } + + /** + * Returns a new EventPayload that will typically encode to less than the specified number of + * bytes. If this `EventPayload` already encodes to fewer bytes it is returned unchanged. + */ + @JvmOverloads + fun trimToSize(maxSizeBytes: Int = DEFAULT_MAX_PAYLOAD_SIZE) { + var json = toByteArray() + if (json.size <= maxSizeBytes) { + return + } + + val event = decodedEvent() + val (itemsTrimmed, dataTrimmed) = event.impl.trimMetadataStringsTo(config.maxStringValueLength) + event.impl.internalMetrics.setMetadataTrimMetrics( + itemsTrimmed, + dataTrimmed + ) + cachedPayloadBytes = null + + json = toByteArray() + if (json.size <= maxSizeBytes) { + cachedPayloadBytes = json + return + } + + val breadcrumbAndBytesRemovedCounts = + event.impl.trimBreadcrumbsBy(json.size - maxSizeBytes) + event.impl.internalMetrics.setBreadcrumbTrimMetrics( + breadcrumbAndBytesRemovedCounts.itemsTrimmed, + breadcrumbAndBytesRemovedCounts.dataTrimmed + ) + cachedPayloadBytes = null } @Throws(IOException::class) @@ -51,4 +111,42 @@ class EventPayload @JvmOverloads internal constructor( writer.endArray() writer.endObject() } + + @Throws(IOException::class) + fun toByteArray(): ByteArray { + var bytes = cachedPayloadBytes + if (bytes == null) { + bytes = JsonHelper.serialize(this) + cachedPayloadBytes = bytes + } + return bytes + } + + /** + * 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 } + } + + companion object { + // 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/InternalReportDelegate.java b/bugsnag-android-core/src/main/java/com/bugsnag/android/InternalReportDelegate.java index a9d6a08d93..83b3ec5d27 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 @@ -3,11 +3,6 @@ import static com.bugsnag.android.DeliveryHeadersKt.HEADER_INTERNAL_ERROR; import static com.bugsnag.android.SeverityReason.REASON_UNHANDLED_EXCEPTION; -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; import android.content.Context; import android.os.Build; @@ -16,6 +11,10 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import com.bugsnag.android.internal.BackgroundTaskService; +import com.bugsnag.android.internal.ImmutableConfig; +import com.bugsnag.android.internal.TaskType; + import java.io.File; import java.io.IOException; import java.util.Date; @@ -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/Session.java b/bugsnag-android-core/src/main/java/com/bugsnag/android/Session.java index 5081ad22e7..ffc6973fe2 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,10 +1,11 @@ package com.bugsnag.android; -import com.bugsnag.android.internal.DateUtils; - import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import com.bugsnag.android.internal.DateUtils; +import com.bugsnag.android.internal.JsonHelper; + import java.io.File; import java.io.IOException; import java.util.ArrayList; @@ -258,6 +259,10 @@ public void toStream(@NonNull JsonStream writer) throws IOException { } } + public byte[] toByteArray() throws IOException { + return JsonHelper.INSTANCE.serialize(this); + } + private void serializePayload(@NonNull JsonStream writer) throws IOException { writer.value(file); } 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-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..34fbdaaab1 --- /dev/null +++ b/bugsnag-plugin-android-okhttp/src/main/java/com/bugsnag/android/okhttp/OkHttpDelivery.kt @@ -0,0 +1,78 @@ +package com.bugsnag.android.okhttp + +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 + +class OkHttpDelivery( + private val client: OkHttpClient = OkHttpClient.Builder().build(), + private val logger: Logger? = null, +) : Delivery { + override fun deliver(payload: Session, deliveryParams: DeliveryParams): DeliveryStatus { + val requestBody = payload.toByteArray().toRequestBody() + + val call = client.newCall( + Request.Builder() + .url(deliveryParams.endpoint) + .headers(deliveryParams.toHeaders()) + .post(requestBody) + .build() + ) + + val response = call.execute() + return DeliveryStatus.forHttpResponseCode(response.code) + } + + override fun deliver(payload: EventPayload, deliveryParams: DeliveryParams): DeliveryStatus { + try { + val requestBody = payload.toByteArray().toRequestBody() + val integrityHeader = payload.integrityToken + + val requestBuilder = Request.Builder() + .url(deliveryParams.endpoint) + + if (integrityHeader != null) { + requestBuilder.header("Bugsnag-Integrity", integrityHeader) + } + + requestBuilder + .headers(deliveryParams.toHeaders()) + .post(requestBody) + + 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 + } + } + + 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..27a4d60f34 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 @@ -41,6 +41,7 @@ static ImmutableConfig generateConfig() throws IOException { 32, 32, 1000, + 10000, 500, LazyKt.lazy(new Function0() { @Override From 00262a7c2ae589295a6d2830fd2eb0b6b718738b Mon Sep 17 00:00:00 2001 From: jason Date: Wed, 23 Oct 2024 08:35:40 +0100 Subject: [PATCH 05/15] refactor(EventPayload): cache the byte[] form of an EventPayload when appropriate --- bugsnag-android-core/detekt-baseline.xml | 12 ++-- .../com/bugsnag/android/DefaultDelivery.kt | 2 +- .../com/bugsnag/android/DeliveryHeaders.kt | 2 - .../com/bugsnag/android/DeliveryStatus.kt | 4 +- .../java/com/bugsnag/android/EventPayload.kt | 55 ++++++++++++------- .../android/InternalReportDelegate.java | 8 +-- .../java/com/bugsnag/android/JsonStream.java | 6 +- .../java/com/bugsnag/android/Session.java | 7 ++- .../bugsnag/android/okhttp/OkHttpDelivery.kt | 2 +- 9 files changed, 58 insertions(+), 40 deletions(-) diff --git a/bugsnag-android-core/detekt-baseline.xml b/bugsnag-android-core/detekt-baseline.xml index e4e75d4c39..e0f143149a 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:EventPayload.kt$EventPayload$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/main/java/com/bugsnag/android/DefaultDelivery.kt b/bugsnag-android-core/src/main/java/com/bugsnag/android/DefaultDelivery.kt index d6be8503eb..945ab127ea 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 @@ -23,7 +23,7 @@ internal class DefaultDelivery( } override fun deliver(payload: EventPayload, deliveryParams: DeliveryParams): DeliveryStatus { - val json = payload.toByteArray() + 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 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 9620699e55..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" 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 a2a168fa3f..4432450a6c 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 @@ -36,7 +36,7 @@ enum class DeliveryStatus { 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 + responseCode != HTTP_CLIENT_TIMEOUT && // except for 408 + responseCode != 429 // and 429 } } 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 fa183dbceb..9ccfaec233 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 @@ -29,7 +29,7 @@ class EventPayload @JvmOverloads internal constructor( internal var eventFile: File? = eventFile private set - private var cachedPayloadBytes: ByteArray? = null + private var cachedBytes: ByteArray? = null private val logger: Logger get() = config.logger @@ -40,12 +40,13 @@ class EventPayload @JvmOverloads internal constructor( internal fun getErrorTypes(): Set { val event = this.event - return event?.impl?.getErrorTypesFromStackframes() - ?: (eventFile?.let { EventFilenameInfo.fromFile(it, config).errorTypes } - ?: emptySet()) + return event?.impl?.getErrorTypesFromStackframes() ?: ( + eventFile?.let { EventFilenameInfo.fromFile(it, config).errorTypes } + ?: emptySet() + ) } - internal fun decodedEvent(): Event { + private fun decodedEvent(): Event { val localEvent = event if (localEvent != null) { return localEvent @@ -61,14 +62,18 @@ class EventPayload @JvmOverloads internal constructor( } /** - * Returns a new EventPayload that will typically encode to less than the specified number of - * bytes. If this `EventPayload` already encodes to fewer bytes it is returned unchanged. + * 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) { + fun trimToSize(maxSizeBytes: Int = DEFAULT_MAX_PAYLOAD_SIZE): EventPayload { var json = toByteArray() if (json.size <= maxSizeBytes) { - return + return this } val event = decodedEvent() @@ -77,12 +82,10 @@ class EventPayload @JvmOverloads internal constructor( itemsTrimmed, dataTrimmed ) - cachedPayloadBytes = null - json = toByteArray() + json = rebuildCachedBytes() if (json.size <= maxSizeBytes) { - cachedPayloadBytes = json - return + return this } val breadcrumbAndBytesRemovedCounts = @@ -91,7 +94,8 @@ class EventPayload @JvmOverloads internal constructor( breadcrumbAndBytesRemovedCounts.itemsTrimmed, breadcrumbAndBytesRemovedCounts.dataTrimmed ) - cachedPayloadBytes = null + + return this } @Throws(IOException::class) @@ -112,14 +116,23 @@ class EventPayload @JvmOverloads internal constructor( 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) fun toByteArray(): ByteArray { - var bytes = cachedPayloadBytes - if (bytes == null) { - bytes = JsonHelper.serialize(this) - cachedPayloadBytes = bytes + var payload = cachedBytes + if (payload == null) { + payload = JsonHelper.serialize(this) + cachedBytes = payload } - return bytes + return payload + } + + private fun rebuildCachedBytes(): ByteArray { + cachedBytes = null + return toByteArray() } /** @@ -146,6 +159,10 @@ class EventPayload @JvmOverloads internal constructor( } 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/InternalReportDelegate.java b/bugsnag-android-core/src/main/java/com/bugsnag/android/InternalReportDelegate.java index 83b3ec5d27..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 @@ -3,6 +3,10 @@ import static com.bugsnag.android.DeliveryHeadersKt.HEADER_INTERNAL_ERROR; import static com.bugsnag.android.SeverityReason.REASON_UNHANDLED_EXCEPTION; +import com.bugsnag.android.internal.BackgroundTaskService; +import com.bugsnag.android.internal.ImmutableConfig; +import com.bugsnag.android.internal.TaskType; + import android.annotation.SuppressLint; import android.content.Context; import android.os.Build; @@ -11,10 +15,6 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; -import com.bugsnag.android.internal.BackgroundTaskService; -import com.bugsnag.android.internal.ImmutableConfig; -import com.bugsnag.android.internal.TaskType; - import java.io.File; import java.io.IOException; import java.util.Date; 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/Session.java b/bugsnag-android-core/src/main/java/com/bugsnag/android/Session.java index ffc6973fe2..45df2cd69f 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,11 +1,11 @@ package com.bugsnag.android; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; - import com.bugsnag.android.internal.DateUtils; import com.bugsnag.android.internal.JsonHelper; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + import java.io.File; import java.io.IOException; import java.util.ArrayList; @@ -259,6 +259,7 @@ public void toStream(@NonNull JsonStream writer) throws IOException { } } + @NonNull public byte[] toByteArray() throws IOException { return JsonHelper.INSTANCE.serialize(this); } 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 index 34fbdaaab1..a0f06690be 100644 --- 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 @@ -33,7 +33,7 @@ class OkHttpDelivery( override fun deliver(payload: EventPayload, deliveryParams: DeliveryParams): DeliveryStatus { try { - val requestBody = payload.toByteArray().toRequestBody() + val requestBody = payload.trimToSize().toByteArray().toRequestBody() val integrityHeader = payload.integrityToken val requestBuilder = Request.Builder() From 152f9a38e27475025c894c05de0805d536f9c98d Mon Sep 17 00:00:00 2001 From: jason Date: Wed, 23 Oct 2024 09:05:20 +0100 Subject: [PATCH 06/15] refactor(OkHttpDelivery): added @JvmOverloads to the OkHttpDelivery constructor --- .../src/main/java/com/bugsnag/android/okhttp/OkHttpDelivery.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 index a0f06690be..e9b9cdf6ea 100644 --- 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 @@ -12,7 +12,7 @@ import okhttp3.Request import okhttp3.RequestBody.Companion.toRequestBody import java.io.IOException -class OkHttpDelivery( +class OkHttpDelivery @JvmOverloads constructor( private val client: OkHttpClient = OkHttpClient.Builder().build(), private val logger: Logger? = null, ) : Delivery { From 583c949ebd9235c7a69c412fe03e110aca113b86 Mon Sep 17 00:00:00 2001 From: jason Date: Wed, 23 Oct 2024 09:07:03 +0100 Subject: [PATCH 07/15] test(OkHttpDelivery): added an end-to-end scenario for OkHttpDelivery --- .../api/bugsnag-android-core.api | 51 ++++++++++++++----- .../com/bugsnag/android/DefaultDelivery.kt | 2 +- .../java/com/bugsnag/android/Deliverable.kt | 36 +++++++++++++ .../java/com/bugsnag/android/EventPayload.kt | 34 ++----------- .../java/com/bugsnag/android/Session.java | 14 +++-- .../bugsnag/android/DeliveryHeadersTest.kt | 11 ++-- .../com/bugsnag/android/DeliveryStatusTest.kt | 22 ++++++++ .../java/com/bugsnag/android/DeliveryTest.kt | 23 --------- .../api/bugsnag-plugin-android-okhttp.api | 9 ++++ .../bugsnag/android/okhttp/OkHttpDelivery.kt | 19 ++++--- .../java/com/bugsnag/android/TestData.java | 2 +- .../jvm-scenarios/detekt-baseline.xml | 1 + .../java/com/bugsnag/android/JavaHooks.java | 2 +- .../scenarios/OkHttpDeliveryScenario.kt | 22 ++++++++ features/full_tests/crash_handler.feature | 9 ++++ 15 files changed, 172 insertions(+), 85 deletions(-) create mode 100644 bugsnag-android-core/src/main/java/com/bugsnag/android/Deliverable.kt create mode 100644 bugsnag-android-core/src/test/java/com/bugsnag/android/DeliveryStatusTest.kt delete mode 100644 bugsnag-android-core/src/test/java/com/bugsnag/android/DeliveryTest.kt create mode 100644 features/fixtures/mazerunner/jvm-scenarios/src/main/java/com/bugsnag/android/mazerunner/scenarios/OkHttpDeliveryScenario.kt diff --git a/bugsnag-android-core/api/bugsnag-android-core.api b/bugsnag-android-core/api/bugsnag-android-core.api index 2ffe8e2f0f..8da871b6f5 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,6 +257,7 @@ 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; @@ -255,6 +265,10 @@ public final class com/bugsnag/android/DeliveryStatus : java/lang/Enum { 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 +409,22 @@ 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 +608,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 +901,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 +919,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 +959,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/src/main/java/com/bugsnag/android/DefaultDelivery.kt b/bugsnag-android-core/src/main/java/com/bugsnag/android/DefaultDelivery.kt index 945ab127ea..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 @@ -15,7 +15,7 @@ internal class DefaultDelivery( val status = deliver( deliveryParams.endpoint, JsonHelper.serialize(payload), - null, + payload.integrityToken, deliveryParams.headers ) logger.i("Session API request finished with status $status") 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..9535ed87bf --- /dev/null +++ b/bugsnag-android-core/src/main/java/com/bugsnag/android/Deliverable.kt @@ -0,0 +1,36 @@ +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 { + @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/EventPayload.kt b/bugsnag-android-core/src/main/java/com/bugsnag/android/EventPayload.kt index 9ccfaec233..787e3d9b0d 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 @@ -5,8 +5,6 @@ import com.bugsnag.android.internal.ImmutableConfig import com.bugsnag.android.internal.JsonHelper import java.io.File import java.io.IOException -import java.security.DigestOutputStream -import java.security.MessageDigest /** * An error report payload. @@ -20,7 +18,7 @@ class EventPayload @JvmOverloads internal constructor( eventFile: File? = null, notifier: Notifier, private val config: ImmutableConfig -) : JsonStream.Streamable { +) : JsonStream.Streamable, Deliverable { @VisibleForTesting internal var event: Event? = event @@ -83,7 +81,7 @@ class EventPayload @JvmOverloads internal constructor( dataTrimmed ) - json = rebuildCachedBytes() + json = rebuildPayloadCache() if (json.size <= maxSizeBytes) { return this } @@ -121,7 +119,7 @@ class EventPayload @JvmOverloads internal constructor( * endpoint (typically configured using [EndpointConfiguration.notify]). */ @Throws(IOException::class) - fun toByteArray(): ByteArray { + override fun toByteArray(): ByteArray { var payload = cachedBytes if (payload == null) { payload = JsonHelper.serialize(this) @@ -130,34 +128,12 @@ class EventPayload @JvmOverloads internal constructor( return payload } - private fun rebuildCachedBytes(): ByteArray { + @VisibleForTesting + internal fun rebuildPayloadCache(): ByteArray { cachedBytes = null return toByteArray() } - /** - * 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 } - } - companion object { /** * The default maximum payload size for [trimToSize], payloads larger than this will 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 45df2cd69f..e5061b645e 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,11 +1,11 @@ package com.bugsnag.android; -import com.bugsnag.android.internal.DateUtils; -import com.bugsnag.android.internal.JsonHelper; - import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import com.bugsnag.android.internal.DateUtils; +import com.bugsnag.android.internal.JsonHelper; + import java.io.File; import java.io.IOException; import java.util.ArrayList; @@ -18,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; @@ -264,6 +264,12 @@ 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/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-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 index e9b9cdf6ea..1dcb1e1f80 100644 --- 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 @@ -18,15 +18,20 @@ class OkHttpDelivery @JvmOverloads constructor( ) : Delivery { override fun deliver(payload: Session, deliveryParams: DeliveryParams): DeliveryStatus { val requestBody = payload.toByteArray().toRequestBody() + val integrityHeader = payload.integrityToken - val call = client.newCall( - Request.Builder() - .url(deliveryParams.endpoint) - .headers(deliveryParams.toHeaders()) - .post(requestBody) - .build() - ) + val requestBuilder = Request.Builder() + .url(deliveryParams.endpoint) + + if (integrityHeader != null) { + requestBuilder.header("Bugsnag-Integrity", integrityHeader) + } + + requestBuilder + .headers(deliveryParams.toHeaders()) + .post(requestBody) + val call = client.newCall(requestBuilder.build()) val response = call.execute() return DeliveryStatus.forHttpResponseCode(response.code) } 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 27a4d60f34..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, diff --git a/features/fixtures/mazerunner/jvm-scenarios/detekt-baseline.xml b/features/fixtures/mazerunner/jvm-scenarios/detekt-baseline.xml index cbe2ac2438..065535747d 100644 --- a/features/fixtures/mazerunner/jvm-scenarios/detekt-baseline.xml +++ b/features/fixtures/mazerunner/jvm-scenarios/detekt-baseline.xml @@ -36,6 +36,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/OkHttpDeliveryScenario.kt b/features/fixtures/mazerunner/jvm-scenarios/src/main/java/com/bugsnag/android/mazerunner/scenarios/OkHttpDeliveryScenario.kt new file mode 100644 index 0000000000..963d1838f4 --- /dev/null +++ b/features/fixtures/mazerunner/jvm-scenarios/src/main/java/com/bugsnag/android/mazerunner/scenarios/OkHttpDeliveryScenario.kt @@ -0,0 +1,22 @@ +package com.bugsnag.android.mazerunner.scenarios + +import android.content.Context +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 { + 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" From 852e5a674e7be7727b650b62abadf584d97728da Mon Sep 17 00:00:00 2001 From: jason Date: Wed, 23 Oct 2024 11:34:58 +0100 Subject: [PATCH 08/15] chore(Session): use Java `default` methods in `interface`s so that Deliverable is more natural to implement from Java --- .../api/bugsnag-android-core.api | 36 ++++++------------- bugsnag-android-core/detekt-baseline.xml | 2 +- .../java/com/bugsnag/android/Deliverable.kt | 3 ++ .../java/com/bugsnag/android/EventPayload.kt | 5 ++- .../java/com/bugsnag/android/Session.java | 12 ++----- .../bugsnag/android/okhttp/OkHttpDelivery.kt | 18 +++++----- .../com/bugsnag/android/BugsnagBuildPlugin.kt | 3 +- 7 files changed, 29 insertions(+), 50 deletions(-) diff --git a/bugsnag-android-core/api/bugsnag-android-core.api b/bugsnag-android-core/api/bugsnag-android-core.api index 8da871b6f5..8b36c5177a 100644 --- a/bugsnag-android-core/api/bugsnag-android-core.api +++ b/bugsnag-android-core/api/bugsnag-android-core.api @@ -237,14 +237,10 @@ public class com/bugsnag/android/Configuration : com/bugsnag/android/CallbackAwa } public abstract interface class com/bugsnag/android/Deliverable { - public abstract fun getIntegrityToken ()Ljava/lang/String; + public 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; @@ -415,7 +411,7 @@ public final class com/bugsnag/android/EventPayload : com/bugsnag/android/Delive 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 fun getIntegrityToken ()Ljava/lang/String; + public final fun getEvent ()Lcom/bugsnag/android/Event; public final fun setApiKey (Ljava/lang/String;)V public fun toByteArray ()[B public fun toStream (Lcom/bugsnag/android/JsonStream;)V @@ -481,25 +477,14 @@ public final class com/bugsnag/android/LastRunInfo { } public abstract interface class com/bugsnag/android/Logger { - public abstract fun d (Ljava/lang/String;)V - public abstract fun d (Ljava/lang/String;Ljava/lang/Throwable;)V - public abstract fun e (Ljava/lang/String;)V - public abstract fun e (Ljava/lang/String;Ljava/lang/Throwable;)V - public abstract fun i (Ljava/lang/String;)V - public abstract fun i (Ljava/lang/String;Ljava/lang/Throwable;)V - public abstract fun w (Ljava/lang/String;)V - public abstract fun w (Ljava/lang/String;Ljava/lang/Throwable;)V -} - -public final class com/bugsnag/android/Logger$DefaultImpls { - public static fun d (Lcom/bugsnag/android/Logger;Ljava/lang/String;)V - public static fun d (Lcom/bugsnag/android/Logger;Ljava/lang/String;Ljava/lang/Throwable;)V - public static fun e (Lcom/bugsnag/android/Logger;Ljava/lang/String;)V - public static fun e (Lcom/bugsnag/android/Logger;Ljava/lang/String;Ljava/lang/Throwable;)V - public static fun i (Lcom/bugsnag/android/Logger;Ljava/lang/String;)V - public static fun i (Lcom/bugsnag/android/Logger;Ljava/lang/String;Ljava/lang/Throwable;)V - public static fun w (Lcom/bugsnag/android/Logger;Ljava/lang/String;)V - public static fun w (Lcom/bugsnag/android/Logger;Ljava/lang/String;Ljava/lang/Throwable;)V + public fun d (Ljava/lang/String;)V + public fun d (Ljava/lang/String;Ljava/lang/Throwable;)V + public fun e (Ljava/lang/String;)V + public fun e (Ljava/lang/String;Ljava/lang/Throwable;)V + public fun i (Ljava/lang/String;)V + public fun i (Ljava/lang/String;Ljava/lang/Throwable;)V + public fun w (Ljava/lang/String;)V + public fun w (Ljava/lang/String;Ljava/lang/Throwable;)V } public class com/bugsnag/android/NativeInterface { @@ -613,7 +598,6 @@ public final class com/bugsnag/android/Session : com/bugsnag/android/Deliverable 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 diff --git a/bugsnag-android-core/detekt-baseline.xml b/bugsnag-android-core/detekt-baseline.xml index e0f143149a..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:EventPayload.kt$EventPayload$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? ) 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 index 9535ed87bf..c41da5de5e 100644 --- a/bugsnag-android-core/src/main/java/com/bugsnag/android/Deliverable.kt +++ b/bugsnag-android-core/src/main/java/com/bugsnag/android/Deliverable.kt @@ -8,6 +8,9 @@ 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 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 787e3d9b0d..6ef6bd7bee 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 @@ -20,9 +20,8 @@ class EventPayload @JvmOverloads internal constructor( private val config: ImmutableConfig ) : JsonStream.Streamable, Deliverable { - @VisibleForTesting - internal var event: Event? = event - private set + var event: Event? = event + internal set internal var eventFile: File? = eventFile private set 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 e5061b645e..b037207596 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,11 +1,11 @@ package com.bugsnag.android; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; - import com.bugsnag.android.internal.DateUtils; import com.bugsnag.android.internal.JsonHelper; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + import java.io.File; import java.io.IOException; import java.util.ArrayList; @@ -264,12 +264,6 @@ 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-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 index 1dcb1e1f80..fcd43c2d28 100644 --- 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 @@ -12,6 +12,8 @@ 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, @@ -22,15 +24,13 @@ class OkHttpDelivery @JvmOverloads constructor( val requestBuilder = Request.Builder() .url(deliveryParams.endpoint) + .headers(deliveryParams.toHeaders()) + .post(requestBody) if (integrityHeader != null) { - requestBuilder.header("Bugsnag-Integrity", integrityHeader) + requestBuilder.header(BUGSNAG_INTEGRITY_HEADER, integrityHeader) } - requestBuilder - .headers(deliveryParams.toHeaders()) - .post(requestBody) - val call = client.newCall(requestBuilder.build()) val response = call.execute() return DeliveryStatus.forHttpResponseCode(response.code) @@ -43,15 +43,13 @@ class OkHttpDelivery @JvmOverloads constructor( val requestBuilder = Request.Builder() .url(deliveryParams.endpoint) + .headers(deliveryParams.toHeaders()) + .post(requestBody) if (integrityHeader != null) { - requestBuilder.header("Bugsnag-Integrity", integrityHeader) + requestBuilder.header(BUGSNAG_INTEGRITY_HEADER, integrityHeader) } - requestBuilder - .headers(deliveryParams.toHeaders()) - .post(requestBody) - val call = client.newCall(requestBuilder.build()) val response = call.execute() diff --git a/buildSrc/src/main/kotlin/com/bugsnag/android/BugsnagBuildPlugin.kt b/buildSrc/src/main/kotlin/com/bugsnag/android/BugsnagBuildPlugin.kt index 3f0d29e096..50600fc698 100644 --- a/buildSrc/src/main/kotlin/com/bugsnag/android/BugsnagBuildPlugin.kt +++ b/buildSrc/src/main/kotlin/com/bugsnag/android/BugsnagBuildPlugin.kt @@ -189,7 +189,8 @@ class BugsnagBuildPlugin : Plugin { freeCompilerArgs += listOf( "-Xno-call-assertions", "-Xno-receiver-assertions", - "-Xno-param-assertions" + "-Xno-param-assertions", + "-Xjvm-default=all" ) } } From aab0c27abc96022247277779102c51b7f04ed349 Mon Sep 17 00:00:00 2001 From: jason Date: Wed, 23 Oct 2024 16:59:41 +0100 Subject: [PATCH 09/15] fix(EventPayload): correctly clear the JSON cache when trimming breadcrumbs in EventPayload.trimToSize --- .../src/main/java/com/bugsnag/android/EventPayload.kt | 1 + 1 file changed, 1 insertion(+) 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 6ef6bd7bee..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 @@ -92,6 +92,7 @@ class EventPayload @JvmOverloads internal constructor( breadcrumbAndBytesRemovedCounts.dataTrimmed ) + rebuildPayloadCache() return this } From 3892c0095ab226e5e413b82f1eeec6971a6939c1 Mon Sep 17 00:00:00 2001 From: jason Date: Wed, 23 Oct 2024 17:41:04 +0100 Subject: [PATCH 10/15] chore(CHANGELOG): added a changelog entry --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index f6856071c4..81f23b975b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,8 @@ * 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) From 4ee30b8dd5ff730e6ec86900e157eb8c0546f6d0 Mon Sep 17 00:00:00 2001 From: jason Date: Thu, 24 Oct 2024 08:29:43 +0100 Subject: [PATCH 11/15] refactor(DeliveryStatus): made DeliveryStatus.forHttpResponseCode JvmStatic for easier use in Java --- bugsnag-android-core/api/bugsnag-android-core.api | 1 + .../main/java/com/bugsnag/android/DeliveryStatus.kt | 11 +++++------ 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/bugsnag-android-core/api/bugsnag-android-core.api b/bugsnag-android-core/api/bugsnag-android-core.api index 8b36c5177a..c00932dcf2 100644 --- a/bugsnag-android-core/api/bugsnag-android-core.api +++ b/bugsnag-android-core/api/bugsnag-android-core.api @@ -257,6 +257,7 @@ public final class com/bugsnag/android/DeliveryStatus : java/lang/Enum { 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; } 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 4432450a6c..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 @@ -26,17 +26,16 @@ enum class DeliveryStatus { FAILURE; companion object { + @JvmStatic fun forHttpResponseCode(responseCode: Int): DeliveryStatus { return when { responseCode in HTTP_OK..299 -> DELIVERED - isUnrecoverableStatusCode(responseCode) -> FAILURE + responseCode in HTTP_BAD_REQUEST..499 && // 400-499 are considered unrecoverable + responseCode != HTTP_CLIENT_TIMEOUT && // except for 408 + responseCode != 429 -> FAILURE + else -> 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 } } From 0aa3b9de13adabe768fa44ee37465c10a4906dde Mon Sep 17 00:00:00 2001 From: jason Date: Fri, 25 Oct 2024 09:59:45 +0100 Subject: [PATCH 12/15] refactor(interfaces): switched back to non-Java interface default methods to avoid breaking binary compatibility --- .../api/bugsnag-android-core.api | 35 ++++++++++++++----- .../java/com/bugsnag/android/Session.java | 6 ++++ .../com/bugsnag/android/BugsnagBuildPlugin.kt | 3 +- 3 files changed, 33 insertions(+), 11 deletions(-) diff --git a/bugsnag-android-core/api/bugsnag-android-core.api b/bugsnag-android-core/api/bugsnag-android-core.api index c00932dcf2..cc0f2f5493 100644 --- a/bugsnag-android-core/api/bugsnag-android-core.api +++ b/bugsnag-android-core/api/bugsnag-android-core.api @@ -237,10 +237,14 @@ public class com/bugsnag/android/Configuration : com/bugsnag/android/CallbackAwa } public abstract interface class com/bugsnag/android/Deliverable { - public fun getIntegrityToken ()Ljava/lang/String; + 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; @@ -413,6 +417,7 @@ public final class com/bugsnag/android/EventPayload : com/bugsnag/android/Delive 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 @@ -478,14 +483,25 @@ public final class com/bugsnag/android/LastRunInfo { } public abstract interface class com/bugsnag/android/Logger { - public fun d (Ljava/lang/String;)V - public fun d (Ljava/lang/String;Ljava/lang/Throwable;)V - public fun e (Ljava/lang/String;)V - public fun e (Ljava/lang/String;Ljava/lang/Throwable;)V - public fun i (Ljava/lang/String;)V - public fun i (Ljava/lang/String;Ljava/lang/Throwable;)V - public fun w (Ljava/lang/String;)V - public fun w (Ljava/lang/String;Ljava/lang/Throwable;)V + public abstract fun d (Ljava/lang/String;)V + public abstract fun d (Ljava/lang/String;Ljava/lang/Throwable;)V + public abstract fun e (Ljava/lang/String;)V + public abstract fun e (Ljava/lang/String;Ljava/lang/Throwable;)V + public abstract fun i (Ljava/lang/String;)V + public abstract fun i (Ljava/lang/String;Ljava/lang/Throwable;)V + public abstract fun w (Ljava/lang/String;)V + public abstract fun w (Ljava/lang/String;Ljava/lang/Throwable;)V +} + +public final class com/bugsnag/android/Logger$DefaultImpls { + public static fun d (Lcom/bugsnag/android/Logger;Ljava/lang/String;)V + public static fun d (Lcom/bugsnag/android/Logger;Ljava/lang/String;Ljava/lang/Throwable;)V + public static fun e (Lcom/bugsnag/android/Logger;Ljava/lang/String;)V + public static fun e (Lcom/bugsnag/android/Logger;Ljava/lang/String;Ljava/lang/Throwable;)V + public static fun i (Lcom/bugsnag/android/Logger;Ljava/lang/String;)V + public static fun i (Lcom/bugsnag/android/Logger;Ljava/lang/String;Ljava/lang/Throwable;)V + public static fun w (Lcom/bugsnag/android/Logger;Ljava/lang/String;)V + public static fun w (Lcom/bugsnag/android/Logger;Ljava/lang/String;Ljava/lang/Throwable;)V } public class com/bugsnag/android/NativeInterface { @@ -599,6 +615,7 @@ public final class com/bugsnag/android/Session : com/bugsnag/android/Deliverable 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 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 b037207596..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 @@ -264,6 +264,12 @@ 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/buildSrc/src/main/kotlin/com/bugsnag/android/BugsnagBuildPlugin.kt b/buildSrc/src/main/kotlin/com/bugsnag/android/BugsnagBuildPlugin.kt index 50600fc698..3f0d29e096 100644 --- a/buildSrc/src/main/kotlin/com/bugsnag/android/BugsnagBuildPlugin.kt +++ b/buildSrc/src/main/kotlin/com/bugsnag/android/BugsnagBuildPlugin.kt @@ -189,8 +189,7 @@ class BugsnagBuildPlugin : Plugin { freeCompilerArgs += listOf( "-Xno-call-assertions", "-Xno-receiver-assertions", - "-Xno-param-assertions", - "-Xjvm-default=all" + "-Xno-param-assertions" ) } } From f2a0afa40043e8357cf4606fa456d668db0d87f6 Mon Sep 17 00:00:00 2001 From: jason Date: Tue, 29 Oct 2024 10:40:36 +0000 Subject: [PATCH 13/15] chore(okhttp): added a TrafficStats tag to the OkHttp delivery --- .../bugsnag/android/okhttp/OkHttpDelivery.kt | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) 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 index fcd43c2d28..85f9c6cdc3 100644 --- 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 @@ -1,5 +1,6 @@ package com.bugsnag.android.okhttp +import android.net.TrafficStats import com.bugsnag.android.Delivery import com.bugsnag.android.DeliveryParams import com.bugsnag.android.DeliveryStatus @@ -32,8 +33,13 @@ class OkHttpDelivery @JvmOverloads constructor( } val call = client.newCall(requestBuilder.build()) - val response = call.execute() - return DeliveryStatus.forHttpResponseCode(response.code) + try { + TrafficStats.setThreadStatsTag(1) + val response = call.execute() + return DeliveryStatus.forHttpResponseCode(response.code) + } finally { + TrafficStats.clearThreadStatsTag() + } } override fun deliver(payload: EventPayload, deliveryParams: DeliveryParams): DeliveryStatus { @@ -51,9 +57,14 @@ class OkHttpDelivery @JvmOverloads constructor( } val call = client.newCall(requestBuilder.build()) - val response = call.execute() - return DeliveryStatus.forHttpResponseCode(response.code) + try { + TrafficStats.setThreadStatsTag(1) + val response = call.execute() + return DeliveryStatus.forHttpResponseCode(response.code) + } finally { + TrafficStats.clearThreadStatsTag() + } } 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 From 2b09ba001482bde165a52b706fec3ab3a79cdefc Mon Sep 17 00:00:00 2001 From: jason Date: Mon, 4 Nov 2024 10:36:17 +0000 Subject: [PATCH 14/15] test(okhttp): turn off StrictMode for the OkHttpDeliveryScenario --- .../bugsnag/android/okhttp/OkHttpDelivery.kt | 37 +++++++++---------- .../android/mazerunner/MazerunnerApp.kt | 1 + .../jvm-scenarios/detekt-baseline.xml | 1 + .../scenarios/OkHttpDeliveryScenario.kt | 7 ++++ 4 files changed, 27 insertions(+), 19 deletions(-) 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 index 85f9c6cdc3..6f5f230dce 100644 --- 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 @@ -20,22 +20,23 @@ class OkHttpDelivery @JvmOverloads constructor( private val logger: Logger? = null, ) : Delivery { override fun deliver(payload: Session, deliveryParams: DeliveryParams): DeliveryStatus { - val requestBody = payload.toByteArray().toRequestBody() - val integrityHeader = payload.integrityToken + 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) + val requestBuilder = Request.Builder() + .url(deliveryParams.endpoint) + .headers(deliveryParams.toHeaders()) + .post(requestBody) - if (integrityHeader != null) { - requestBuilder.header(BUGSNAG_INTEGRITY_HEADER, integrityHeader) - } + if (integrityHeader != null) { + requestBuilder.header(BUGSNAG_INTEGRITY_HEADER, integrityHeader) + } - val call = client.newCall(requestBuilder.build()) - try { - TrafficStats.setThreadStatsTag(1) + val call = client.newCall(requestBuilder.build()) val response = call.execute() + return DeliveryStatus.forHttpResponseCode(response.code) } finally { TrafficStats.clearThreadStatsTag() @@ -43,6 +44,7 @@ class OkHttpDelivery @JvmOverloads constructor( } override fun deliver(payload: EventPayload, deliveryParams: DeliveryParams): DeliveryStatus { + TrafficStats.setThreadStatsTag(1) try { val requestBody = payload.trimToSize().toByteArray().toRequestBody() val integrityHeader = payload.integrityToken @@ -58,13 +60,8 @@ class OkHttpDelivery @JvmOverloads constructor( val call = client.newCall(requestBuilder.build()) - try { - TrafficStats.setThreadStatsTag(1) - val response = call.execute() - return DeliveryStatus.forHttpResponseCode(response.code) - } finally { - TrafficStats.clearThreadStatsTag() - } + 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 @@ -77,6 +74,8 @@ class OkHttpDelivery @JvmOverloads constructor( } catch (exception: Exception) { logger?.w("Unexpected error delivering payload", exception) return DeliveryStatus.FAILURE + } finally { + TrafficStats.clearThreadStatsTag() } } 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 065535747d..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() 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 index 963d1838f4..e26e31a794 100644 --- 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 @@ -1,6 +1,7 @@ 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 @@ -12,6 +13,12 @@ internal class OkHttpDeliveryScenario( ) : 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() } From 36914e0612fe5c70028e783f6f2a836326b00da0 Mon Sep 17 00:00:00 2001 From: YYChen01988 Date: Thu, 7 Nov 2024 09:58:15 +0000 Subject: [PATCH 15/15] release v6.9.0 --- CHANGELOG.md | 2 +- .../src/main/java/com/bugsnag/android/Notifier.kt | 2 +- examples/sdk-app-example/app/build.gradle | 4 ++-- gradle.properties | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 81f23b975b..d01509630f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,6 @@ # Changelog -## TBD +## 6.9.0 (2024-11-07) ### Enhancements 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/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/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