From 84b38e291a3b3a56ee71dfe278bf6d02d7cf0d50 Mon Sep 17 00:00:00 2001 From: hayaksi1 <193020925+hayaksi1@users.noreply.github.com> Date: Mon, 10 Aug 2026 22:53:50 +0300 Subject: [PATCH] Fix bug reports failing to send after the app has crashed The whole crash call stack was appended to the bug report description, and the bug report server copies that description verbatim into the GitHub issue it opens. GitHub refuses an issue body over 65536 characters, so the server's save step failed and answered "Could not save report". The stored crash data is only cleared after a successful send, so every retry failed the same way. Truncate the description before sending it. The user's own text and the GitHub issue link come first, so only the tail of the crash stack is dropped. The uncaught exception handler already writes the full stack through Timber, so it stays available in the attached log files. The 40 KB budget leaves room for the other reported fields, mainly the list of omitted log file names, which is not otherwise bounded. --- .../android/appconfig/RageshakeConfig.kt | 7 ++ .../impl/reporter/DefaultBugReporter.kt | 11 +++- .../impl/reporter/DefaultBugReporterTest.kt | 64 +++++++++++++++++++ 3 files changed, 81 insertions(+), 1 deletion(-) diff --git a/appconfig/src/main/kotlin/io/element/android/appconfig/RageshakeConfig.kt b/appconfig/src/main/kotlin/io/element/android/appconfig/RageshakeConfig.kt index 6dd2143d95..82d2145ecc 100644 --- a/appconfig/src/main/kotlin/io/element/android/appconfig/RageshakeConfig.kt +++ b/appconfig/src/main/kotlin/io/element/android/appconfig/RageshakeConfig.kt @@ -35,4 +35,11 @@ object RageshakeConfig { * The maximum number of log lines a rageshake can contain. */ const val MAX_LOG_LINES_SIZE = 1_000_000 + + /** + * The maximum size of the description. The bug report server copies it verbatim into a GitHub issue body, + * which GitHub rejects above 65536 characters. The remaining budget is left to the other reported fields, + * mainly the list of the omitted log file names. + */ + const val MAX_DESCRIPTION_SIZE = 40 * 1024 } diff --git a/features/rageshake/impl/src/main/kotlin/io/element/android/features/rageshake/impl/reporter/DefaultBugReporter.kt b/features/rageshake/impl/src/main/kotlin/io/element/android/features/rageshake/impl/reporter/DefaultBugReporter.kt index 96bd7adfce..c1000784fc 100755 --- a/features/rageshake/impl/src/main/kotlin/io/element/android/features/rageshake/impl/reporter/DefaultBugReporter.kt +++ b/features/rageshake/impl/src/main/kotlin/io/element/android/features/rageshake/impl/reporter/DefaultBugReporter.kt @@ -152,7 +152,7 @@ class DefaultBugReporter( append("\n\n\n\n--------------------------------- crash call stack ---------------------------------\n") append(crashCallStack) } - } + }.truncateDescription() val gzippedFiles = mutableListOf() var filesTooBig = emptyList() @@ -494,4 +494,13 @@ class DefaultBugReporter( private fun countLogLines(file: File): Int { return file.reader().useLines { it.count() } } + + private fun String.truncateDescription(): String { + return if (length <= RageshakeConfig.MAX_DESCRIPTION_SIZE) { + this + } else { + take(RageshakeConfig.MAX_DESCRIPTION_SIZE) + + "\n\n--------------------------------- truncated, see the attached logs ---------------------------------" + } + } } diff --git a/features/rageshake/impl/src/test/kotlin/io/element/android/features/rageshake/impl/reporter/DefaultBugReporterTest.kt b/features/rageshake/impl/src/test/kotlin/io/element/android/features/rageshake/impl/reporter/DefaultBugReporterTest.kt index ad29e5ed3f..fe32678c27 100755 --- a/features/rageshake/impl/src/test/kotlin/io/element/android/features/rageshake/impl/reporter/DefaultBugReporterTest.kt +++ b/features/rageshake/impl/src/test/kotlin/io/element/android/features/rageshake/impl/reporter/DefaultBugReporterTest.kt @@ -309,6 +309,68 @@ class DefaultBugReporterTest : RobolectricTest() { assertThat(foundValues["label"]).isEqualTo("crash") } + @Test + fun `test sendBugReport truncates a description which is too long`() = runTest { + val server = MockWebServer() + server.enqueue( + MockResponse() + .setResponseCode(200) + ) + server.start() + val hugeCrashCallStack = "a".repeat(RageshakeConfig.MAX_DESCRIPTION_SIZE * 2) + val sut = createDefaultBugReporter( + server = server, + crashDataStore = FakeCrashDataStore(hugeCrashCallStack, true), + ) + + sut.sendBugReport( + withDevicesLogs = true, + withCrashLogs = true, + withScreenshot = true, + problemDescription = "a bug occurred", + canContact = true, + listener = NoopBugReporterListener(), + ) + val request = server.takeRequest() + + val text = collectValuesFromFormData(request)["text"]!! + assertThat(text).startsWith("a bug occurred") + assertThat(text).contains("truncated") + assertThat(text.length).isAtMost(RageshakeConfig.MAX_DESCRIPTION_SIZE + TRUNCATION_MARKER_MAX_SIZE) + assertThat(text.length).isLessThan(GITHUB_MAX_ISSUE_BODY_SIZE) + server.shutdown() + } + + @Test + fun `test sendBugReport does not truncate a description which fits`() = runTest { + val server = MockWebServer() + server.enqueue( + MockResponse() + .setResponseCode(200) + ) + server.start() + val sut = createDefaultBugReporter( + server = server, + crashDataStore = FakeCrashDataStore("I did crash", true), + ) + + sut.sendBugReport( + withDevicesLogs = true, + withCrashLogs = true, + withScreenshot = true, + problemDescription = "a bug occurred", + canContact = true, + listener = NoopBugReporterListener(), + ) + val request = server.takeRequest() + + val text = collectValuesFromFormData(request)["text"]!! + assertThat(text).startsWith("a bug occurred") + assertThat(text).endsWith("I did crash") + assertThat(text).doesNotContain("truncated") + server.shutdown() + } + private fun collectValuesFromFormData(request: RecordedRequest): HashMap { val boundary = request.headers["Content-Type"]!!.split("=").last() val foundValues = HashMap() @@ -535,5 +597,7 @@ class DefaultBugReporterTest : RobolectricTest() { companion object { private const val EXPECTED_NUMBER_OF_PROGRESS_VALUE = 18 + private const val GITHUB_MAX_ISSUE_BODY_SIZE = 65536 + private const val TRUNCATION_MARKER_MAX_SIZE = 200 } }