Skip to content

Commit

Permalink
Feature flag for autofill site breakage reporting
Browse files Browse the repository at this point in the history
  • Loading branch information
CDRussell committed Jul 17, 2024
1 parent fee754a commit 674ec35
Show file tree
Hide file tree
Showing 9 changed files with 443 additions and 0 deletions.
Original file line number Diff line number Diff line change
@@ -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<Preferences>,
) : 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
}
}
Original file line number Diff line number Diff line change
@@ -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<FeatureExceptions.FeatureException>) {
repository.updateAllExceptions(
exception.map { AutofillSiteBreakageReportingEntity(domain = it.domain, reason = it.reason.orEmpty()) },
)
}
}
Original file line number Diff line number Diff line change
@@ -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
}
Original file line number Diff line number Diff line change
@@ -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()
}
}
Original file line number Diff line number Diff line change
@@ -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))
}
}
}
}
}
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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()
}
}
Original file line number Diff line number Diff line change
@@ -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<Migration>()

@Dao
abstract class AutofillSiteBreakageReportingDao {

@Insert(onConflict = OnConflictStrategy.REPLACE)
abstract fun insertAll(domains: List<AutofillSiteBreakageReportingEntity>)

@Transaction
open fun updateAll(domains: List<AutofillSiteBreakageReportingEntity>) {
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<AutofillSiteBreakageReportingEntity>

@Query("delete from autofill_site_breakage_reporting")
abstract fun deleteAll()
}
Loading

0 comments on commit 674ec35

Please sign in to comment.