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 24, 2024
1 parent 6160f09 commit 8dde33f
Show file tree
Hide file tree
Showing 9 changed files with 445 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.reporting.remoteconfig.AutofillSiteBreakageReporting
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(
@AutofillSiteBreakageReporting 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 = 42
}
}
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,43 @@
/*
* 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
import com.duckduckgo.feature.toggles.api.Toggle

@ContributesRemoteFeature(
scope = AppScope::class,
boundType = AutofillSiteBreakageReportingFeature::class,
featureName = "autofillBreakageReporter",
settingsStore = AutofillSiteBreakageReportingRemoteSettingsPersister::class,
exceptionsStore = AutofillSiteBreakageReportingExceptionsPersister::class,
)
/**
* 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, and always true for internal builds
*
* If the remote feature is not present defaults to `false`
*/

@Toggle.InternalAlwaysEnabled
@Toggle.DefaultValue(false)
fun self(): Toggle
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
/*
* 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.datastore.core.DataStore
import androidx.datastore.preferences.core.Preferences
import androidx.datastore.preferences.preferencesDataStore
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 javax.inject.Qualifier
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()
}

private val Context.autofillSiteBreakageReportingDataStore: DataStore<Preferences> by preferencesDataStore(
name = "autofill_site_breakage_reporting",
)

@Provides
@SingleInstanceIn(AppScope::class)
@AutofillSiteBreakageReporting
fun provideImportPasswordsDesktopSyncDataStore(context: Context): DataStore<Preferences> {
return context.autofillSiteBreakageReportingDataStore
}
}

@Qualifier
annotation class AutofillSiteBreakageReporting
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,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
Expand Up @@ -17,6 +17,7 @@
package com.duckduckgo.feature.toggles.api.toggle

import com.duckduckgo.autofill.api.AutofillFeature
import com.duckduckgo.autofill.impl.reporting.remoteconfig.AutofillSiteBreakageReportingFeature
import com.duckduckgo.feature.toggles.api.Toggle

class AutofillTestFeature : AutofillFeature {
Expand All @@ -37,6 +38,12 @@ class AutofillTestFeature : AutofillFeature {
override fun onByDefault(): Toggle = TestToggle(onByDefault)
}

class AutofillReportBreakageTestFeature : AutofillSiteBreakageReportingFeature {
var topLevelFeatureEnabled: Boolean = false

override fun self() = TestToggle(topLevelFeatureEnabled)
}

open class TestToggle(val enabled: Boolean) : Toggle {
override fun getRawStoredState(): Toggle.State? = null
override fun setEnabled(state: Toggle.State) {}
Expand Down
Loading

0 comments on commit 8dde33f

Please sign in to comment.