Allow uploading notification push rules in bug reports (#5538)

* Allow uploading push rules in bug reports

* Improve bug report screen previews

* Update screenshots

---------

Co-authored-by: ElementBot <android@element.io>
This commit is contained in:
Jorge Martin Espinosa 2025-10-15 11:45:04 +02:00 committed by GitHub
parent 35cf3aeb0b
commit 5b1bfac6ff
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
34 changed files with 116 additions and 38 deletions

View file

@ -18,6 +18,7 @@ interface BugReporter {
* @param withScreenshot true to include the screenshot
* @param problemDescription the bug description
* @param canContact true if the user opt in to be contacted directly
* @param sendPushRules true to include the push rules
* @param listener the listener
*/
suspend fun sendBugReport(
@ -26,6 +27,7 @@ interface BugReporter {
withScreenshot: Boolean,
problemDescription: String,
canContact: Boolean = false,
sendPushRules: Boolean = false,
listener: BugReporterListener
)

View file

@ -16,4 +16,5 @@ sealed interface BugReportEvents {
data class SetSendLog(val sendLog: Boolean) : BugReportEvents
data class SetCanContact(val canContact: Boolean) : BugReportEvents
data class SetSendScreenshot(val sendScreenshot: Boolean) : BugReportEvents
data class SetSendPushRules(val sendPushRules: Boolean) : BugReportEvents
}

View file

@ -105,6 +105,9 @@ class BugReportPresenter(
is BugReportEvents.SetSendScreenshot -> updateFormState(formState) {
copy(sendScreenshot = event.sendScreenshot)
}
is BugReportEvents.SetSendPushRules -> updateFormState(formState) {
copy(sendPushRules = event.sendPushRules)
}
BugReportEvents.ClearError -> {
sendingProgress.floatValue = 0f
sendingAction.value = AsyncAction.Uninitialized
@ -137,6 +140,7 @@ class BugReportPresenter(
withScreenshot = formState.sendScreenshot,
problemDescription = formState.description,
canContact = formState.canContact,
sendPushRules = formState.sendPushRules,
listener = listener
)
}

View file

@ -29,14 +29,16 @@ data class BugReportFormState(
val description: String,
val sendLogs: Boolean,
val canContact: Boolean,
val sendScreenshot: Boolean
val sendScreenshot: Boolean,
val sendPushRules: Boolean,
) : Parcelable {
companion object {
val Default = BugReportFormState(
description = "",
sendLogs = true,
canContact = false,
sendScreenshot = false
sendScreenshot = false,
sendPushRules = false,
)
}
}

View file

@ -7,6 +7,7 @@
package io.element.android.features.rageshake.impl.bugreport
import android.content.res.Configuration
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
@ -26,6 +27,7 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.KeyboardCapitalization
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.unit.dp
import coil3.compose.AsyncImage
@ -40,7 +42,6 @@ import io.element.android.libraries.designsystem.components.preferences.Preferen
import io.element.android.libraries.designsystem.components.preferences.PreferenceSwitch
import io.element.android.libraries.designsystem.modifiers.onTabOrEnterKeyFocusNext
import io.element.android.libraries.designsystem.preview.ElementPreview
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
import io.element.android.libraries.designsystem.theme.components.Button
import io.element.android.libraries.designsystem.theme.components.ListItem
import io.element.android.libraries.designsystem.theme.components.Text
@ -142,6 +143,13 @@ fun BugReportView(
}
}
}
PreferenceSwitch(
isChecked = state.formState.sendPushRules,
onCheckedChange = { eventSink(BugReportEvents.SetSendPushRules(it)) },
enabled = isFormEnabled,
title = stringResource(R.string.screen_bug_report_send_notification_settings_title),
subtitle = stringResource(R.string.screen_bug_report_send_notification_settings_description),
)
// Submit
PreferenceRow {
Button(
@ -174,9 +182,20 @@ fun BugReportView(
}
}
@PreviewsDayNight
@Preview(heightDp = 1000)
@Composable
internal fun BugReportViewPreview(@PreviewParameter(BugReportStateProvider::class) state: BugReportState) = ElementPreview {
internal fun BugReportViewDayPreview(@PreviewParameter(BugReportStateProvider::class) state: BugReportState) = ElementPreview {
BugReportView(
state = state,
onSuccess = {},
onBackClick = {},
onViewLogs = {},
)
}
@Preview(heightDp = 1000, uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
internal fun BugReportViewNightPreview(@PreviewParameter(BugReportStateProvider::class) state: BugReportState) = ElementPreview {
BugReportView(
state = state,
onSuccess = {},

View file

@ -44,6 +44,7 @@ import okhttp3.MediaType.Companion.toMediaTypeOrNull
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.RequestBody.Companion.asRequestBody
import okhttp3.RequestBody.Companion.toRequestBody
import okhttp3.Response
import org.json.JSONException
import org.json.JSONObject
@ -113,6 +114,7 @@ class DefaultBugReporter(
withScreenshot: Boolean,
problemDescription: String,
canContact: Boolean,
sendPushRules: Boolean,
listener: BugReporterListener,
) {
val url = bugReporterUrlProvider.provide().first()
@ -181,6 +183,16 @@ class DefaultBugReporter(
if (curveKey != null && edKey != null) {
builder.addFormDataPart("device_keys", "curve25519:$curveKey, ed25519:$edKey")
}
if (sendPushRules) {
client.notificationSettingsService.getRawPushRules().getOrNull()?.let { pushRules ->
builder.addFormDataPart(
name = "file",
filename = "push_rules.json",
body = pushRules.toByteArray().toRequestBody(MimeTypes.Json.toMediaTypeOrNull())
)
}
}
}
}
if (crashCallStack.isNotEmpty() && withCrashLogs) {

View file

@ -14,7 +14,7 @@
<string name="screen_bug_report_include_screenshot">"Send screenshot"</string>
<string name="screen_bug_report_logs_description">"Logs will be included with your message to make sure that everything is working properly. To send your message without logs, turn off this setting."</string>
<string name="screen_bug_report_rash_logs_alert_title">"%1$s crashed the last time it was used. Would you like to share a crash report with us?"</string>
<string name="screen_bug_report_send_notification_settings_description">"If you are having issues with notifications, uploading the notification settings can help us pinpoint the root cause."</string>
<string name="screen_bug_report_send_notification_settings_description">"If you are having issues with notifications, uploading the notification push rules can help us pinpoint the root cause. Note these rules can contain private information, such as your display name or keywords to be notified for."</string>
<string name="screen_bug_report_send_notification_settings_title">"Send notification settings"</string>
<string name="screen_bug_report_view_logs">"View logs"</string>
</resources>

View file

@ -106,6 +106,20 @@ class BugReportPresenterTest {
}
}
@Test
fun `present - send notification settings`() = runTest {
val presenter = createPresenter()
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
val initialState = awaitItem()
initialState.eventSink.invoke(BugReportEvents.SetSendPushRules(true))
assertThat(awaitItem().formState).isEqualTo(BugReportFormState.Default.copy(sendPushRules = true))
initialState.eventSink.invoke(BugReportEvents.SetSendPushRules(false))
assertThat(awaitItem().formState).isEqualTo(BugReportFormState.Default.copy(sendPushRules = false))
}
}
@Test
fun `present - reset all`() = runTest {
val presenter = createPresenter(

View file

@ -26,6 +26,7 @@ class FakeBugReporter(val mode: Mode = Mode.Success) : BugReporter {
withScreenshot: Boolean,
problemDescription: String,
canContact: Boolean,
sendPushRules: Boolean,
listener: BugReporterListener,
) {
delay(100)

View file

@ -24,6 +24,7 @@ import io.element.android.libraries.matrix.test.FakeSdkMetadata
import io.element.android.libraries.matrix.test.auth.FakeMatrixAuthenticationService
import io.element.android.libraries.matrix.test.core.aBuildMeta
import io.element.android.libraries.matrix.test.encryption.FakeEncryptionService
import io.element.android.libraries.matrix.test.notificationsettings.FakeNotificationSettingsService
import io.element.android.libraries.matrix.test.tracing.FakeTracingService
import io.element.android.libraries.network.useragent.DefaultUserAgentProvider
import io.element.android.libraries.sessionstorage.api.SessionStore
@ -65,6 +66,7 @@ class DefaultBugReporterTest {
withDevicesLogs = true,
withCrashLogs = true,
withScreenshot = true,
sendPushRules = true,
problemDescription = "a bug occurred",
canContact = true,
listener = object : BugReporterListener {
@ -109,7 +111,12 @@ class DefaultBugReporterTest {
)
val fakeEncryptionService = FakeEncryptionService()
val matrixClient = FakeMatrixClient(encryptionService = fakeEncryptionService)
val fakePushRules = "{ content: ... }"
val fakeNotificationSettingsService = FakeNotificationSettingsService(
getRawPushRulesResult = { Result.success(fakePushRules) }
)
val matrixClient = FakeMatrixClient(encryptionService = fakeEncryptionService, notificationSettingsService = fakeNotificationSettingsService)
fakeEncryptionService.givenDeviceKeys("CURVECURVECURVE", "EDKEYEDKEYEDKY")
val sut = createDefaultBugReporter(
@ -124,6 +131,7 @@ class DefaultBugReporterTest {
withDevicesLogs = true,
withCrashLogs = true,
withScreenshot = true,
sendPushRules = true,
problemDescription = "a bug occurred",
canContact = true,
listener = object : BugReporterListener {
@ -149,9 +157,11 @@ class DefaultBugReporterTest {
assertThat(foundValues["user_id"]).isEqualTo("@foo:example.com")
assertThat(foundValues["text"]).isEqualTo("a bug occurred")
assertThat(foundValues["device_keys"]).isEqualTo("curve25519:CURVECURVECURVE, ed25519:EDKEYEDKEYEDKY")
assertThat(foundValues["file"]).contains(fakePushRules)
// device_key now added given they are not null
assertThat(progressValues.size).isEqualTo(EXPECTED_NUMBER_OF_PROGRESS_VALUE + 1)
// so is the push_rules value
assertThat(progressValues.size).isEqualTo(EXPECTED_NUMBER_OF_PROGRESS_VALUE + 2)
server.shutdown()
}
@ -272,6 +282,7 @@ class DefaultBugReporterTest {
withDevicesLogs = true,
withCrashLogs = true,
withScreenshot = true,
sendPushRules = true,
problemDescription = "a bug occurred",
canContact = true,
listener = object : BugReporterListener {

View file

@ -13,6 +13,7 @@ import io.element.android.libraries.core.bool.orFalse
@Suppress("ktlint:standard:property-naming")
object MimeTypes {
const val Any: String = "*/*"
const val Json = "application/json"
const val OctetStream = "application/octet-stream"
const val Apk = "application/vnd.android.package-archive"
const val Pdf = "application/pdf"

View file

@ -33,4 +33,5 @@ interface NotificationSettingsService {
suspend fun setInviteForMeEnabled(enabled: Boolean): Result<Unit>
suspend fun getRoomsWithUserDefinedRules(): Result<List<String>>
suspend fun canHomeServerPushEncryptedEventsToDevice(): Result<Boolean>
suspend fun getRawPushRules(): Result<String?>
}

View file

@ -139,4 +139,8 @@ class RustNotificationSettingsService(
runCatchingExceptions {
notificationSettings.await().canPushEncryptedEventToDevice()
}
override suspend fun getRawPushRules(): Result<String?> = runCatchingExceptions {
notificationSettings.await().getRawPushRules()
}
}

View file

@ -13,6 +13,7 @@ import io.element.android.libraries.matrix.api.room.RoomNotificationMode
import io.element.android.libraries.matrix.api.room.RoomNotificationSettings
import io.element.android.libraries.matrix.test.A_ROOM_ID
import io.element.android.libraries.matrix.test.A_ROOM_NOTIFICATION_MODE
import io.element.android.tests.testutils.lambda.lambdaError
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharedFlow
@ -23,6 +24,7 @@ class FakeNotificationSettingsService(
initialEncryptedGroupDefaultMode: RoomNotificationMode = RoomNotificationMode.MENTIONS_AND_KEYWORDS_ONLY,
initialOneToOneDefaultMode: RoomNotificationMode = RoomNotificationMode.ALL_MESSAGES,
initialEncryptedOneToOneDefaultMode: RoomNotificationMode = RoomNotificationMode.ALL_MESSAGES,
private val getRawPushRulesResult: () -> Result<String> = { lambdaError() },
) : NotificationSettingsService {
private val notificationSettingsStateFlow = MutableStateFlow(Unit)
private var defaultGroupRoomNotificationMode: RoomNotificationMode = initialGroupDefaultMode
@ -178,4 +180,8 @@ class FakeNotificationSettingsService(
fun givenCanHomeServerPushEncryptedEventsToDeviceResult(result: Result<Boolean>) {
canHomeServerPushEncryptedEventsToDeviceResult = result
}
override suspend fun getRawPushRules(): Result<String?> {
return getRawPushRulesResult()
}
}

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:3c229f0f963e247de755f45e55432191f88a51ba79baf49f0002c2c12dbdc09e
size 48613

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:cbf1430985870c99c6e6a2b427257235219250e1c6805bde44de64d1382bd30a
size 113544

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:5bc84691892f956b6d282ae5aff0a7ef7900aad754bb7f04e25582c25a256342
size 45892

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:3c229f0f963e247de755f45e55432191f88a51ba79baf49f0002c2c12dbdc09e
size 48613

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:26a5a053b88110ed76a59a99a199ada720fd7a44ff3b95546c4993ae3d8edddf
size 37496

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:c1020f77055e3369f012f73159be9e8c7bf582d4211b3b22f5409ecba46b6898
size 47180

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:47c2e74119bd6f2329f15ee3b7dbf0115c69293bd34ed5c2e67de77319b7f820
size 111436

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:984d43efa545fb9f104ea6d4eba87e768ade025eee8d1f74260049a64c7e7a7c
size 44401

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:c1020f77055e3369f012f73159be9e8c7bf582d4211b3b22f5409ecba46b6898
size 47180

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:41c5eb671aaa7c8666942dbc808eb7b03f81cbde98906c5c17cdee61b1d9566b
size 35446

View file

@ -1,3 +0,0 @@
version https://git-lfs.github.com/spec/v1
oid sha256:c56f65335dcc9cda18ed7f647bf1dc9428ee07762ea3d40e7d28731a81ca8f2e
size 69552

View file

@ -1,3 +0,0 @@
version https://git-lfs.github.com/spec/v1
oid sha256:12d9e6e297bbb3429a4fe6dd3ac16847280e479fe89e96ec78f35159a51d01f0
size 108813

View file

@ -1,3 +0,0 @@
version https://git-lfs.github.com/spec/v1
oid sha256:9b9e42889c2b31dfee71a644b2da66c0b7cf4f4f475030d7aab3a7d500e7246a
size 66366

View file

@ -1,3 +0,0 @@
version https://git-lfs.github.com/spec/v1
oid sha256:c56f65335dcc9cda18ed7f647bf1dc9428ee07762ea3d40e7d28731a81ca8f2e
size 69552

View file

@ -1,3 +0,0 @@
version https://git-lfs.github.com/spec/v1
oid sha256:57402ade9b70c2124b6764edbaac71d30b89c49eea8175c448cbe7520867a42f
size 49569

View file

@ -1,3 +0,0 @@
version https://git-lfs.github.com/spec/v1
oid sha256:38b5fb1099f10dae154270b5df1f52f4018d355d04e0040ba9c06ebe57f259c6
size 67628

View file

@ -1,3 +0,0 @@
version https://git-lfs.github.com/spec/v1
oid sha256:1a5920a102082f35d0f0a7171687998f568da5e2d68a9f9d9b5884c55c832bd2
size 106479

View file

@ -1,3 +0,0 @@
version https://git-lfs.github.com/spec/v1
oid sha256:5035c2ffa678e1c761030fbac56dfb3afd043c25a3df0cd6fb7c83b2b311c2ae
size 63678

View file

@ -1,3 +0,0 @@
version https://git-lfs.github.com/spec/v1
oid sha256:38b5fb1099f10dae154270b5df1f52f4018d355d04e0040ba9c06ebe57f259c6
size 67628

View file

@ -1,3 +0,0 @@
version https://git-lfs.github.com/spec/v1
oid sha256:946751ac246e3e833ec39da86b25d80766daed29449b1d0fcebe33c5079b724a
size 46418