From 674ec351d2ea6c391cf9198d4ce6882fbf81d9bd Mon Sep 17 00:00:00 2001 From: Craig Russell Date: Tue, 9 Jul 2024 14:46:19 +0100 Subject: [PATCH] Feature flag for autofill site breakage reporting --- .../AutofillSiteBreakageReportingDataStore.kt | 57 +++++++++++++++ ...iteBreakageReportingExceptionsPersister.kt | 37 ++++++++++ .../AutofillSiteBreakageReportingFeature.kt | 33 +++++++++ .../AutofillSiteBreakageReportingModule.kt | 58 ++++++++++++++++ ...reakageReportingRemoteSettingsPersister.kt | 50 ++++++++++++++ ...ageReportingRemoteFeatureCodegenTrigger.kt | 30 ++++++++ ...ageReportingRemoteSettingsPersisterTest.kt | 53 ++++++++++++++ .../AutofillSiteBreakageExceptionDatabase.kt | 69 +++++++++++++++++++ ...lSiteBreakageReportingFeatureRepository.kt | 56 +++++++++++++++ 9 files changed, 443 insertions(+) create mode 100644 autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/reporting/AutofillSiteBreakageReportingDataStore.kt create mode 100644 autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/reporting/remoteconfig/AutofillSiteBreakageReportingExceptionsPersister.kt create mode 100644 autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/reporting/remoteconfig/AutofillSiteBreakageReportingFeature.kt create mode 100644 autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/reporting/remoteconfig/AutofillSiteBreakageReportingModule.kt create mode 100644 autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/reporting/remoteconfig/AutofillSiteBreakageReportingRemoteSettingsPersister.kt create mode 100644 autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/reporting/remoteconfig/UnusedAutofillSiteBreakageReportingRemoteFeatureCodegenTrigger.kt create mode 100644 autofill/autofill-impl/src/test/java/com/duckduckgo/autofill/impl/reporting/remoteconfig/AutofillSiteBreakageReportingRemoteSettingsPersisterTest.kt create mode 100644 autofill/autofill-store/src/main/java/com/duckduckgo/autofill/store/reporting/AutofillSiteBreakageExceptionDatabase.kt create mode 100644 autofill/autofill-store/src/main/java/com/duckduckgo/autofill/store/reporting/AutofillSiteBreakageReportingFeatureRepository.kt diff --git a/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/reporting/AutofillSiteBreakageReportingDataStore.kt b/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/reporting/AutofillSiteBreakageReportingDataStore.kt new file mode 100644 index 000000000000..92136a6df1fc --- /dev/null +++ b/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/reporting/AutofillSiteBreakageReportingDataStore.kt @@ -0,0 +1,57 @@ +/* + * Copyright (c) 2024 DuckDuckGo + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.duckduckgo.autofill.impl.reporting + +import androidx.datastore.core.DataStore +import androidx.datastore.preferences.core.Preferences +import androidx.datastore.preferences.core.edit +import androidx.datastore.preferences.core.intPreferencesKey +import com.duckduckgo.autofill.impl.ui.credential.management.importpassword.desktopapp.ImportPasswordDesktopSync +import com.duckduckgo.di.scopes.AppScope +import com.squareup.anvil.annotations.ContributesBinding +import dagger.SingleInstanceIn +import javax.inject.Inject +import kotlinx.coroutines.flow.firstOrNull + +interface AutofillSiteBreakageReportingDataStore { + + suspend fun getMinimumNumberOfDaysBeforeReportPromptReshown(): Int + suspend fun updateMinimumNumberOfDaysBeforeReportPromptReshown(newValue: Int) +} + +@SingleInstanceIn(AppScope::class) +@ContributesBinding(AppScope::class) +class AutofillSiteBreakageReportingDataStoreImpl @Inject constructor( + @ImportPasswordDesktopSync private val store: DataStore, +) : AutofillSiteBreakageReportingDataStore { + + private val daysBeforePromptShownAgainKey = intPreferencesKey("days_before_prompt_shown_again") + + override suspend fun updateMinimumNumberOfDaysBeforeReportPromptReshown(newValue: Int) { + store.edit { + it[daysBeforePromptShownAgainKey] = newValue + } + } + + override suspend fun getMinimumNumberOfDaysBeforeReportPromptReshown(): Int { + return store.data.firstOrNull()?.get(daysBeforePromptShownAgainKey) ?: DEFAULT_NUMBER_OF_DAYS_BEFORE_REPORT_PROMPT_RESHOWN + } + + companion object { + private const val DEFAULT_NUMBER_OF_DAYS_BEFORE_REPORT_PROMPT_RESHOWN = 0 + } +} diff --git a/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/reporting/remoteconfig/AutofillSiteBreakageReportingExceptionsPersister.kt b/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/reporting/remoteconfig/AutofillSiteBreakageReportingExceptionsPersister.kt new file mode 100644 index 000000000000..86b4ca318729 --- /dev/null +++ b/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/reporting/remoteconfig/AutofillSiteBreakageReportingExceptionsPersister.kt @@ -0,0 +1,37 @@ +/* + * Copyright (c) 2024 DuckDuckGo + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.duckduckgo.autofill.impl.reporting.remoteconfig + +import com.duckduckgo.autofill.store.reporting.AutofillSiteBreakageReportingEntity +import com.duckduckgo.autofill.store.reporting.AutofillSiteBreakageReportingFeatureRepository +import com.duckduckgo.di.scopes.AppScope +import com.duckduckgo.feature.toggles.api.FeatureExceptions +import com.duckduckgo.feature.toggles.api.RemoteFeatureStoreNamed +import com.squareup.anvil.annotations.ContributesBinding +import javax.inject.Inject + +@ContributesBinding(AppScope::class) +@RemoteFeatureStoreNamed(AutofillSiteBreakageReportingFeature::class) +class AutofillSiteBreakageReportingExceptionsPersister @Inject constructor( + private val repository: AutofillSiteBreakageReportingFeatureRepository, +) : FeatureExceptions.Store { + override fun insertAll(exception: List) { + repository.updateAllExceptions( + exception.map { AutofillSiteBreakageReportingEntity(domain = it.domain, reason = it.reason.orEmpty()) }, + ) + } +} diff --git a/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/reporting/remoteconfig/AutofillSiteBreakageReportingFeature.kt b/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/reporting/remoteconfig/AutofillSiteBreakageReportingFeature.kt new file mode 100644 index 000000000000..125766214c35 --- /dev/null +++ b/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/reporting/remoteconfig/AutofillSiteBreakageReportingFeature.kt @@ -0,0 +1,33 @@ +/* + * Copyright (c) 2024 DuckDuckGo + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.duckduckgo.autofill.impl.reporting.remoteconfig + +import com.duckduckgo.feature.toggles.api.Toggle + +/** + * This is the class that represents the feature flag for offering to report Autofill breakages + */ +interface AutofillSiteBreakageReportingFeature { + /** + * @return `true` when the remote config has the global "autofillBreakageReporter" feature flag enabled + * + * If the remote feature is not present defaults to `false` + */ + + @Toggle.DefaultValue(false) + fun self(): Toggle +} diff --git a/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/reporting/remoteconfig/AutofillSiteBreakageReportingModule.kt b/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/reporting/remoteconfig/AutofillSiteBreakageReportingModule.kt new file mode 100644 index 000000000000..2074c3e6bde6 --- /dev/null +++ b/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/reporting/remoteconfig/AutofillSiteBreakageReportingModule.kt @@ -0,0 +1,58 @@ +/* + * Copyright (c) 2024 DuckDuckGo + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.duckduckgo.autofill.impl.reporting.remoteconfig + +import android.content.Context +import androidx.room.Room +import com.duckduckgo.app.di.AppCoroutineScope +import com.duckduckgo.app.di.IsMainProcess +import com.duckduckgo.autofill.store.reporting.ALL_MIGRATIONS +import com.duckduckgo.autofill.store.reporting.AutofillSiteBreakageReportingDatabase +import com.duckduckgo.autofill.store.reporting.AutofillSiteBreakageReportingFeatureRepository +import com.duckduckgo.autofill.store.reporting.AutofillSiteBreakageReportingFeatureRepositoryImpl +import com.duckduckgo.common.utils.DispatcherProvider +import com.duckduckgo.di.scopes.AppScope +import com.squareup.anvil.annotations.ContributesTo +import dagger.Module +import dagger.Provides +import dagger.SingleInstanceIn +import kotlinx.coroutines.CoroutineScope + +@Module +@ContributesTo(AppScope::class) +class AutofillSiteBreakageReportingModule { + + @SingleInstanceIn(AppScope::class) + @Provides + fun repository( + database: AutofillSiteBreakageReportingDatabase, + @AppCoroutineScope appCoroutineScope: CoroutineScope, + dispatcherProvider: DispatcherProvider, + @IsMainProcess isMainProcess: Boolean, + ): AutofillSiteBreakageReportingFeatureRepository { + return AutofillSiteBreakageReportingFeatureRepositoryImpl(database, appCoroutineScope, dispatcherProvider, isMainProcess) + } + + @Provides + @SingleInstanceIn(AppScope::class) + fun database(context: Context): AutofillSiteBreakageReportingDatabase { + return Room.databaseBuilder(context, AutofillSiteBreakageReportingDatabase::class.java, "autofillSiteBreakageReporting.db") + .fallbackToDestructiveMigration() + .addMigrations(*ALL_MIGRATIONS) + .build() + } +} diff --git a/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/reporting/remoteconfig/AutofillSiteBreakageReportingRemoteSettingsPersister.kt b/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/reporting/remoteconfig/AutofillSiteBreakageReportingRemoteSettingsPersister.kt new file mode 100644 index 000000000000..50b3031550c6 --- /dev/null +++ b/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/reporting/remoteconfig/AutofillSiteBreakageReportingRemoteSettingsPersister.kt @@ -0,0 +1,50 @@ +/* + * Copyright (c) 2024 DuckDuckGo + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.duckduckgo.autofill.impl.reporting.remoteconfig + +import com.duckduckgo.app.di.AppCoroutineScope +import com.duckduckgo.autofill.impl.reporting.AutofillSiteBreakageReportingDataStore +import com.duckduckgo.common.utils.DispatcherProvider +import com.duckduckgo.di.scopes.AppScope +import com.duckduckgo.feature.toggles.api.FeatureSettings +import com.duckduckgo.feature.toggles.api.RemoteFeatureStoreNamed +import com.squareup.anvil.annotations.ContributesBinding +import javax.inject.Inject +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch +import org.json.JSONObject + +@ContributesBinding(AppScope::class) +@RemoteFeatureStoreNamed(AutofillSiteBreakageReportingFeature::class) +class AutofillSiteBreakageReportingRemoteSettingsPersister @Inject constructor( + private val dataStore: AutofillSiteBreakageReportingDataStore, + @AppCoroutineScope private val appCoroutineScope: CoroutineScope, + private val dispatchers: DispatcherProvider, +) : FeatureSettings.Store { + + override fun store(jsonString: String) { + appCoroutineScope.launch(dispatchers.io()) { + val json = JSONObject(jsonString) + + "monitorIntervalDays".let { + if (json.has(it)) { + dataStore.updateMinimumNumberOfDaysBeforeReportPromptReshown(json.getInt(it)) + } + } + } + } +} diff --git a/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/reporting/remoteconfig/UnusedAutofillSiteBreakageReportingRemoteFeatureCodegenTrigger.kt b/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/reporting/remoteconfig/UnusedAutofillSiteBreakageReportingRemoteFeatureCodegenTrigger.kt new file mode 100644 index 000000000000..16061a50708a --- /dev/null +++ b/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/reporting/remoteconfig/UnusedAutofillSiteBreakageReportingRemoteFeatureCodegenTrigger.kt @@ -0,0 +1,30 @@ +/* + * Copyright (c) 2024 DuckDuckGo + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.duckduckgo.autofill.impl.reporting.remoteconfig + +import com.duckduckgo.anvil.annotations.ContributesRemoteFeature +import com.duckduckgo.di.scopes.AppScope + +@ContributesRemoteFeature( + scope = AppScope::class, + boundType = AutofillSiteBreakageReportingFeature::class, + featureName = "autofillBreakageReporter", + settingsStore = AutofillSiteBreakageReportingRemoteSettingsPersister::class, + exceptionsStore = AutofillSiteBreakageReportingExceptionsPersister::class, +) +@Suppress("unused") +private interface UnusedAutofillSiteBreakageReportingRemoteFeatureCodegenTrigger diff --git a/autofill/autofill-impl/src/test/java/com/duckduckgo/autofill/impl/reporting/remoteconfig/AutofillSiteBreakageReportingRemoteSettingsPersisterTest.kt b/autofill/autofill-impl/src/test/java/com/duckduckgo/autofill/impl/reporting/remoteconfig/AutofillSiteBreakageReportingRemoteSettingsPersisterTest.kt new file mode 100644 index 000000000000..ff40ed4405b4 --- /dev/null +++ b/autofill/autofill-impl/src/test/java/com/duckduckgo/autofill/impl/reporting/remoteconfig/AutofillSiteBreakageReportingRemoteSettingsPersisterTest.kt @@ -0,0 +1,53 @@ +package com.duckduckgo.autofill.impl.reporting.remoteconfig + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.duckduckgo.autofill.impl.reporting.AutofillSiteBreakageReportingDataStore +import com.duckduckgo.common.test.CoroutineTestRule +import kotlinx.coroutines.test.runTest +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.kotlin.any +import org.mockito.kotlin.mock +import org.mockito.kotlin.never +import org.mockito.kotlin.verify + +@RunWith(AndroidJUnit4::class) +class AutofillSiteBreakageReportingRemoteSettingsPersisterTest { + + @get:Rule + val coroutineTestRule: CoroutineTestRule = CoroutineTestRule() + + private val dataStore: AutofillSiteBreakageReportingDataStore = mock() + + private val testee = AutofillSiteBreakageReportingRemoteSettingsPersister( + dataStore = dataStore, + appCoroutineScope = coroutineTestRule.testScope, + dispatchers = coroutineTestRule.testDispatcherProvider, + ) + + @Test + fun whenSettingsIsEmptyStringThenNothingStored() = runTest { + testee.store("") + verify(dataStore, never()).updateMinimumNumberOfDaysBeforeReportPromptReshown(any()) + } + + @Test + fun whenSettingsIsEmptyThenNothingStored() = runTest { + testee.store("{}") + verify(dataStore, never()).updateMinimumNumberOfDaysBeforeReportPromptReshown(any()) + } + + @Test + fun whenSettingsIsSpecificThenThatValueIsStored() = runTest { + testee.store(validJson(10)) + verify(dataStore).updateMinimumNumberOfDaysBeforeReportPromptReshown(10) + } + + @Suppress("SameParameterValue") + private fun validJson(numberDays: Int): String { + return """ + {"monitorIntervalDays": $numberDays} + """.trimIndent() + } +} diff --git a/autofill/autofill-store/src/main/java/com/duckduckgo/autofill/store/reporting/AutofillSiteBreakageExceptionDatabase.kt b/autofill/autofill-store/src/main/java/com/duckduckgo/autofill/store/reporting/AutofillSiteBreakageExceptionDatabase.kt new file mode 100644 index 000000000000..d501e00c2bd1 --- /dev/null +++ b/autofill/autofill-store/src/main/java/com/duckduckgo/autofill/store/reporting/AutofillSiteBreakageExceptionDatabase.kt @@ -0,0 +1,69 @@ +/* + * Copyright (c) 2024 DuckDuckGo + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.duckduckgo.autofill.store.reporting + +import androidx.room.Dao +import androidx.room.Database +import androidx.room.Entity +import androidx.room.Insert +import androidx.room.OnConflictStrategy +import androidx.room.PrimaryKey +import androidx.room.Query +import androidx.room.RoomDatabase +import androidx.room.Transaction +import androidx.room.migration.Migration + +@Database( + exportSchema = true, + version = 1, + entities = [ + AutofillSiteBreakageReportingEntity::class, + ], +) +abstract class AutofillSiteBreakageReportingDatabase : RoomDatabase() { + abstract fun dao(): AutofillSiteBreakageReportingDao +} + +@Entity(tableName = "autofill_site_breakage_reporting") +data class AutofillSiteBreakageReportingEntity( + @PrimaryKey val domain: String, + val reason: String, +) + +val ALL_MIGRATIONS = emptyArray() + +@Dao +abstract class AutofillSiteBreakageReportingDao { + + @Insert(onConflict = OnConflictStrategy.REPLACE) + abstract fun insertAll(domains: List) + + @Transaction + open fun updateAll(domains: List) { + deleteAll() + insertAll(domains) + } + + @Query("select * from autofill_site_breakage_reporting where domain = :domain") + abstract fun get(domain: String): AutofillSiteBreakageReportingEntity + + @Query("select * from autofill_site_breakage_reporting") + abstract fun getAll(): List + + @Query("delete from autofill_site_breakage_reporting") + abstract fun deleteAll() +} diff --git a/autofill/autofill-store/src/main/java/com/duckduckgo/autofill/store/reporting/AutofillSiteBreakageReportingFeatureRepository.kt b/autofill/autofill-store/src/main/java/com/duckduckgo/autofill/store/reporting/AutofillSiteBreakageReportingFeatureRepository.kt new file mode 100644 index 000000000000..fb92adaf1e6b --- /dev/null +++ b/autofill/autofill-store/src/main/java/com/duckduckgo/autofill/store/reporting/AutofillSiteBreakageReportingFeatureRepository.kt @@ -0,0 +1,56 @@ +/* + * Copyright (c) 2024 DuckDuckGo + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.duckduckgo.autofill.store.reporting + +import com.duckduckgo.common.utils.DispatcherProvider +import java.util.concurrent.CopyOnWriteArrayList +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch + +interface AutofillSiteBreakageReportingFeatureRepository { + fun updateAllExceptions(exceptions: List) + val exceptions: CopyOnWriteArrayList +} + +class AutofillSiteBreakageReportingFeatureRepositoryImpl( + val database: AutofillSiteBreakageReportingDatabase, + coroutineScope: CoroutineScope, + dispatcherProvider: DispatcherProvider, + isMainProcess: Boolean, +) : AutofillSiteBreakageReportingFeatureRepository { + + private val dao = database.dao() + override val exceptions = CopyOnWriteArrayList() + + init { + coroutineScope.launch(dispatcherProvider.io()) { + if (isMainProcess) { + loadToMemory() + } + } + } + + override fun updateAllExceptions(exceptions: List) { + dao.updateAll(exceptions) + loadToMemory() + } + + private fun loadToMemory() { + exceptions.clear() + dao.getAll().map { exceptions.add(it.domain) } + } +}