From 38ef6eb40ba8d0a1fc261ee9d17f87752ecc9fe0 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Tue, 14 Oct 2025 09:49:20 +0200 Subject: [PATCH 1/2] Add number of accounts info in the rageshake data. --- .../impl/reporter/DefaultBugReporter.kt | 2 + .../impl/reporter/DefaultBugReporterTest.kt | 71 ++++++++++++++++++- 2 files changed, 72 insertions(+), 1 deletion(-) 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 5787939688..ba3af00a2f 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 @@ -153,6 +153,7 @@ class DefaultBugReporter( } } val sessionData = sessionStore.getLatestSession() + val numberOfAccounts = sessionStore.getAllSessions().size val deviceId = sessionData?.deviceId ?: "undefined" val userId = sessionData?.userId?.let { UserId(it) } // build the multi part request @@ -161,6 +162,7 @@ class DefaultBugReporter( .addFormDataPart("app", RageshakeConfig.BUG_REPORT_APP_NAME) .addFormDataPart("user_agent", userAgentProvider.provide()) .addFormDataPart("user_id", userId?.toString() ?: "undefined") + .addFormDataPart("number_of_accounts", numberOfAccounts.toString()) .addFormDataPart("can_contact", canContact.toString()) .addFormDataPart("device_id", deviceId) .addFormDataPart("device", Build.MODEL.trim()) 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 ecb8d13b12..1bca9d021f 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 @@ -18,6 +18,8 @@ import io.element.android.libraries.matrix.api.MatrixClientProvider import io.element.android.libraries.matrix.api.auth.MatrixAuthenticationService import io.element.android.libraries.matrix.api.tracing.TracingService import io.element.android.libraries.matrix.api.tracing.WriteToFilesConfiguration +import io.element.android.libraries.matrix.test.A_DEVICE_ID +import io.element.android.libraries.matrix.test.A_USER_ID import io.element.android.libraries.matrix.test.FakeMatrixClient import io.element.android.libraries.matrix.test.FakeMatrixClientProvider import io.element.android.libraries.matrix.test.FakeSdkMetadata @@ -147,6 +149,72 @@ class DefaultBugReporterTest { assertThat(foundValues["device_id"]).isEqualTo("ABCDEFGH") assertThat(foundValues["sdk_sha"]).isEqualTo("123456789") assertThat(foundValues["user_id"]).isEqualTo("@foo:example.com") + assertThat(foundValues["number_of_accounts"]).isEqualTo("1") + assertThat(foundValues["text"]).isEqualTo("a bug occurred") + assertThat(foundValues["device_keys"]).isEqualTo("curve25519:CURVECURVECURVE, ed25519:EDKEYEDKEYEDKY") + + // device_key now added given they are not null + assertThat(progressValues.size).isEqualTo(EXPECTED_NUMBER_OF_PROGRESS_VALUE + 1) + + server.shutdown() + } + + @Test + fun `test sendBugReport multi accounts`() = runTest { + val server = MockWebServer() + server.enqueue( + MockResponse() + .setResponseCode(200) + ) + server.start() + + val mockSessionStore = InMemorySessionStore( + initialList = listOf( + aSessionData(sessionId = "@foo:example.com", deviceId = "ABCDEFGH"), + aSessionData(sessionId = A_USER_ID.value, deviceId = A_DEVICE_ID.value), + ) + ) + + val fakeEncryptionService = FakeEncryptionService() + val matrixClient = FakeMatrixClient(encryptionService = fakeEncryptionService) + + fakeEncryptionService.givenDeviceKeys("CURVECURVECURVE", "EDKEYEDKEYEDKY") + val sut = createDefaultBugReporter( + server = server, + crashDataStore = FakeCrashDataStore(), + sessionStore = mockSessionStore, + matrixClientProvider = FakeMatrixClientProvider(getClient = { Result.success(matrixClient) }) + ) + + val progressValues = mutableListOf() + sut.sendBugReport( + withDevicesLogs = true, + withCrashLogs = true, + withScreenshot = true, + problemDescription = "a bug occurred", + canContact = true, + listener = object : BugReporterListener { + override fun onUploadCancelled() {} + + override fun onUploadFailed(reason: String?) {} + + override fun onProgress(progress: Int) { + progressValues.add(progress) + } + + override fun onUploadSucceed() {} + }, + ) + val request = server.takeRequest() + + val foundValues = collectValuesFromFormData(request) + + assertThat(foundValues["app"]).isEqualTo(RageshakeConfig.BUG_REPORT_APP_NAME) + assertThat(foundValues["can_contact"]).isEqualTo("true") + assertThat(foundValues["device_id"]).isEqualTo("ABCDEFGH") + assertThat(foundValues["sdk_sha"]).isEqualTo("123456789") + assertThat(foundValues["user_id"]).isEqualTo("@foo:example.com") + assertThat(foundValues["number_of_accounts"]).isEqualTo("2") assertThat(foundValues["text"]).isEqualTo("a bug occurred") assertThat(foundValues["device_keys"]).isEqualTo("curve25519:CURVECURVECURVE, ed25519:EDKEYEDKEYEDKY") @@ -228,6 +296,7 @@ class DefaultBugReporterTest { assertThat(foundValues["device_keys"]).isNull() assertThat(foundValues["device_id"]).isEqualTo("undefined") assertThat(foundValues["user_id"]).isEqualTo("undefined") + assertThat(foundValues["number_of_accounts"]).isEqualTo("0") assertThat(foundValues["label"]).isEqualTo("crash") } @@ -474,6 +543,6 @@ class DefaultBugReporterTest { } companion object { - private const val EXPECTED_NUMBER_OF_PROGRESS_VALUE = 17 + private const val EXPECTED_NUMBER_OF_PROGRESS_VALUE = 18 } } From bd177084a5a521570849abc6e7f1d14fb1bfbec4 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Tue, 14 Oct 2025 14:30:52 +0200 Subject: [PATCH 2/2] Remove comment. Default values will be detected by the existing Konsist test `Data class state MUST not have default value` --- .../screens/changeaccountprovider/ChangeAccountProviderState.kt | 1 - .../screens/chooseaccountprovider/ChooseAccountProviderState.kt | 1 - .../confirmaccountprovider/ConfirmAccountProviderState.kt | 1 - .../screens/searchaccountprovider/SearchAccountProviderState.kt | 1 - .../preferences/impl/analytics/AnalyticsSettingsState.kt | 1 - .../securebackup/impl/enter/SecureBackupEnterRecoveryKeyState.kt | 1 - .../features/securebackup/impl/setup/SecureBackupSetupState.kt | 1 - .../io/element/android/features/signedout/impl/SignedOutState.kt | 1 - .../fileTemplates/Template Presentation Classes.kt.child.3.kt | 1 - 9 files changed, 9 deletions(-) diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/changeaccountprovider/ChangeAccountProviderState.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/changeaccountprovider/ChangeAccountProviderState.kt index f6f62eca01..4cd939824e 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/changeaccountprovider/ChangeAccountProviderState.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/changeaccountprovider/ChangeAccountProviderState.kt @@ -11,7 +11,6 @@ import io.element.android.features.login.impl.accountprovider.AccountProvider import io.element.android.features.login.impl.changeserver.ChangeServerState import kotlinx.collections.immutable.ImmutableList -// Do not use default value, so no member get forgotten in the presenters. data class ChangeAccountProviderState( val accountProviders: ImmutableList, val canSearchForAccountProviders: Boolean, diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/chooseaccountprovider/ChooseAccountProviderState.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/chooseaccountprovider/ChooseAccountProviderState.kt index ad4677ea24..e7df3256d6 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/chooseaccountprovider/ChooseAccountProviderState.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/chooseaccountprovider/ChooseAccountProviderState.kt @@ -12,7 +12,6 @@ import io.element.android.features.login.impl.login.LoginMode import io.element.android.libraries.architecture.AsyncData import kotlinx.collections.immutable.ImmutableList -// Do not use default value, so no member get forgotten in the presenters. data class ChooseAccountProviderState( val accountProviders: ImmutableList, val selectedAccountProvider: AccountProvider?, diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/confirmaccountprovider/ConfirmAccountProviderState.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/confirmaccountprovider/ConfirmAccountProviderState.kt index d0da9ab85e..3bde254432 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/confirmaccountprovider/ConfirmAccountProviderState.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/confirmaccountprovider/ConfirmAccountProviderState.kt @@ -11,7 +11,6 @@ import io.element.android.features.login.impl.accountprovider.AccountProvider import io.element.android.features.login.impl.login.LoginMode import io.element.android.libraries.architecture.AsyncData -// Do not use default value, so no member get forgotten in the presenters. data class ConfirmAccountProviderState( val accountProvider: AccountProvider, val isAccountCreation: Boolean, diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/searchaccountprovider/SearchAccountProviderState.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/searchaccountprovider/SearchAccountProviderState.kt index 4fb0a08141..1cb029caae 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/searchaccountprovider/SearchAccountProviderState.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/searchaccountprovider/SearchAccountProviderState.kt @@ -11,7 +11,6 @@ import io.element.android.features.login.impl.changeserver.ChangeServerState import io.element.android.features.login.impl.resolver.HomeserverData import io.element.android.libraries.architecture.AsyncData -// Do not use default value, so no member get forgotten in the presenters. data class SearchAccountProviderState( val userInput: String, val userInputResult: AsyncData>, diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/analytics/AnalyticsSettingsState.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/analytics/AnalyticsSettingsState.kt index 7f0cbb4672..1fd15a8e8a 100644 --- a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/analytics/AnalyticsSettingsState.kt +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/analytics/AnalyticsSettingsState.kt @@ -9,7 +9,6 @@ package io.element.android.features.preferences.impl.analytics import io.element.android.features.analytics.api.preferences.AnalyticsPreferencesState -// Do not use default value, so no member get forgotten in the presenters. data class AnalyticsSettingsState( val analyticsPreferencesState: AnalyticsPreferencesState, ) diff --git a/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/enter/SecureBackupEnterRecoveryKeyState.kt b/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/enter/SecureBackupEnterRecoveryKeyState.kt index 60b73b37a3..aaefd09436 100644 --- a/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/enter/SecureBackupEnterRecoveryKeyState.kt +++ b/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/enter/SecureBackupEnterRecoveryKeyState.kt @@ -10,7 +10,6 @@ package io.element.android.features.securebackup.impl.enter import io.element.android.features.securebackup.impl.setup.views.RecoveryKeyViewState import io.element.android.libraries.architecture.AsyncAction -// Do not use default value, so no member get forgotten in the presenters. data class SecureBackupEnterRecoveryKeyState( val recoveryKeyViewState: RecoveryKeyViewState, val isSubmitEnabled: Boolean, diff --git a/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/setup/SecureBackupSetupState.kt b/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/setup/SecureBackupSetupState.kt index 91f710d07d..7df1bbfa84 100644 --- a/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/setup/SecureBackupSetupState.kt +++ b/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/setup/SecureBackupSetupState.kt @@ -9,7 +9,6 @@ package io.element.android.features.securebackup.impl.setup import io.element.android.features.securebackup.impl.setup.views.RecoveryKeyViewState -// Do not use default value, so no member get forgotten in the presenters. data class SecureBackupSetupState( val isChangeRecoveryKeyUserStory: Boolean, val recoveryKeyViewState: RecoveryKeyViewState, diff --git a/features/signedout/impl/src/main/kotlin/io/element/android/features/signedout/impl/SignedOutState.kt b/features/signedout/impl/src/main/kotlin/io/element/android/features/signedout/impl/SignedOutState.kt index 725654ca57..b72a20c12f 100644 --- a/features/signedout/impl/src/main/kotlin/io/element/android/features/signedout/impl/SignedOutState.kt +++ b/features/signedout/impl/src/main/kotlin/io/element/android/features/signedout/impl/SignedOutState.kt @@ -9,7 +9,6 @@ package io.element.android.features.signedout.impl import io.element.android.libraries.sessionstorage.api.SessionData -// Do not use default value, so no member get forgotten in the presenters. data class SignedOutState( val appName: String, val signedOutSession: SessionData?, diff --git a/tools/templates/files/fileTemplates/Template Presentation Classes.kt.child.3.kt b/tools/templates/files/fileTemplates/Template Presentation Classes.kt.child.3.kt index 3fcdd7f219..d8b57da6b6 100644 --- a/tools/templates/files/fileTemplates/Template Presentation Classes.kt.child.3.kt +++ b/tools/templates/files/fileTemplates/Template Presentation Classes.kt.child.3.kt @@ -1,7 +1,6 @@ #if (${PACKAGE_NAME} && ${PACKAGE_NAME} != "")package ${PACKAGE_NAME}#end // TODO add your ui models. Remove the eventSink if you don't have events. -// Do not use default value, so no member get forgotten in the presenters. data class ${NAME}State( val eventSink: (${NAME}Events) -> Unit )