Skip to content

Commit

Permalink
Add pixels to blocklist experiments (#5202)
Browse files Browse the repository at this point in the history
Task/Issue URL:
https://app.asana.com/0/1125189844152671/1208618649788232/f

### Description
Adds pixels for blocklist experiments as well as a new parameter to the
breakage form

### Steps to test this PR

- [x] Change remote config to URL
`https://jsonblob.com/api/1298581151181299712`
- [x] Install the app, finish the onboarding
- [x] Filter by `pixel sent`
- [x] Go to a website
- [x] Refresh it two times
- [x] A bunch of pixels like
`experiment_metrics_tdsNextExperimentBaseline_treatment with params:
{metric=2xRefresh, value=1, enrollmentDate=2024-10-28,
conversionWindowDays=0} {}` should be sent.
- [x] Refresh three times
- [x] A bunch of pixels like
`experiment_metrics_tdsNextExperimentBaseline_treatment with params:
{metric=3xRefresh, value=1, enrollmentDate=2024-10-28,
conversionWindowDays=0} {}` should be sent.
- [x] Send a broken site report
- [x] The epbf pixel should contain
`blockListExperiment=tdsNextExperimentBaseline_treatment`
- [x] In the broken site report, disable protections
- [x] No pixel with the name
`experiment_metrics_tdsNextExperimentBaseline_treatment` should be sent.
- [x] Re-enable protections
- [x] Go to the privacy dashboard, website not working and disable
protections from there.
- [x] No pixel with the name
`experiment_metrics_tdsNextExperimentBaseline_treatment` should be sent.
- [x] Re-enable protections
- [x] Go to the privacy dashboard, disable protections from there
- [x] A bunch of pixels like
`experiment_metrics_tdsNextExperimentBaseline_treatment with params:
{metric=privacyToggleUsed, value=1, enrollmentDate=2024-10-28,
conversionWindowDays=0} {}` should be sent
  • Loading branch information
marcosholgado authored Oct 29, 2024
1 parent d056ad8 commit 725ff6d
Show file tree
Hide file tree
Showing 22 changed files with 862 additions and 135 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,7 @@ import com.duckduckgo.common.test.CoroutineTestRule
import com.duckduckgo.common.test.InstantSchedulersRule
import com.duckduckgo.common.utils.DispatcherProvider
import com.duckduckgo.common.utils.device.DeviceInfo
import com.duckduckgo.common.utils.plugins.PluginPoint
import com.duckduckgo.downloads.api.DownloadStateListener
import com.duckduckgo.downloads.api.FileDownloader
import com.duckduckgo.downloads.api.FileDownloader.PendingFileDownload
Expand Down Expand Up @@ -222,6 +223,8 @@ import com.duckduckgo.privacy.config.impl.features.gpc.RealGpc
import com.duckduckgo.privacy.config.impl.features.gpc.RealGpc.Companion.GPC_HEADER
import com.duckduckgo.privacy.config.impl.features.gpc.RealGpc.Companion.GPC_HEADER_VALUE
import com.duckduckgo.privacy.config.store.features.gpc.GpcRepository
import com.duckduckgo.privacy.dashboard.api.PrivacyProtectionTogglePlugin
import com.duckduckgo.privacy.dashboard.api.PrivacyToggleOrigin
import com.duckduckgo.privacy.dashboard.impl.pixels.PrivacyDashboardPixels
import com.duckduckgo.privacyprotectionspopup.api.PrivacyProtectionsPopupExperimentExternalPixels
import com.duckduckgo.privacyprotectionspopup.api.PrivacyProtectionsPopupManager
Expand Down Expand Up @@ -490,6 +493,8 @@ class BrowserTabViewModelTest {
private val mockAutoCompleteRepository: AutoCompleteRepository = mock()
private val changeOmnibarPositionFeature: ChangeOmnibarPositionFeature = mock()
private val mockHighlightsOnboardingExperimentManager: HighlightsOnboardingExperimentManager = mock()
private val protectionTogglePlugin = FakePrivacyProtectionTogglePlugin()
private val protectionTogglePluginPoint = FakePluginPoint(protectionTogglePlugin)
private var fakeAndroidConfigBrowserFeature = FakeFeatureToggleFactory.create(AndroidBrowserConfigFeature::class.java)
private val mockAutocompleteTabsFeature: AutocompleteTabsFeature = mock()

Expand Down Expand Up @@ -662,6 +667,7 @@ class BrowserTabViewModelTest {
refreshPixelSender = refreshPixelSender,
changeOmnibarPositionFeature = changeOmnibarPositionFeature,
highlightsOnboardingExperimentManager = mockHighlightsOnboardingExperimentManager,
privacyProtectionTogglePlugin = protectionTogglePluginPoint,
)

testee.loadData("abc", null, false, false)
Expand Down Expand Up @@ -1999,6 +2005,7 @@ class BrowserTabViewModelTest {
testee.onPrivacyProtectionMenuClicked()
verify(mockUserAllowListRepository).addDomainToUserAllowList("www.example.com")
verify(mockPixel).fire(AppPixelName.BROWSER_MENU_ALLOWLIST_ADD)
assertEquals(1, protectionTogglePlugin.toggleOff)
}

@Test
Expand All @@ -2018,6 +2025,7 @@ class BrowserTabViewModelTest {
testee.onPrivacyProtectionMenuClicked()
verify(mockUserAllowListRepository).removeDomainFromUserAllowList("www.example.com")
verify(mockPixel).fire(AppPixelName.BROWSER_MENU_ALLOWLIST_REMOVE)
assertEquals(1, protectionTogglePlugin.toggleOn)
}

@Test
Expand Down Expand Up @@ -5349,6 +5357,8 @@ class BrowserTabViewModelTest {

verify(mockPixel).fire(AppPixelName.BROWSER_MENU_ALLOWLIST_ADD, params, type = Count)
verify(mockPixel).fire(AppPixelName.BROWSER_MENU_ALLOWLIST_REMOVE, params, type = Count)
assertEquals(1, protectionTogglePlugin.toggleOff)
assertEquals(1, protectionTogglePlugin.toggleOn)
verify(privacyProtectionsPopupExperimentExternalPixels).tryReportProtectionsToggledFromBrowserMenu(protectionsEnabled = false)
verify(privacyProtectionsPopupExperimentExternalPixels).tryReportProtectionsToggledFromBrowserMenu(protectionsEnabled = true)
}
Expand Down Expand Up @@ -6353,4 +6363,22 @@ class BrowserTabViewModelTest {
override suspend fun canGeneratePasswordFromWebView(url: String) = enabled
override suspend fun canAccessCredentialManagementScreen() = enabled
}

class FakePluginPoint(val plugin: FakePrivacyProtectionTogglePlugin) : PluginPoint<PrivacyProtectionTogglePlugin> {
override fun getPlugins(): Collection<PrivacyProtectionTogglePlugin> {
return listOf(plugin)
}
}

class FakePrivacyProtectionTogglePlugin : PrivacyProtectionTogglePlugin {
var toggleOff = 0
var toggleOn = 0

override suspend fun onToggleOff(origin: PrivacyToggleOrigin) {
toggleOff++
}
override suspend fun onToggleOn(origin: PrivacyToggleOrigin) {
toggleOn++
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import com.duckduckgo.app.pixels.AppPixelName
import com.duckduckgo.app.privacy.db.UserAllowListRepository
import com.duckduckgo.app.statistics.pixels.Pixel
import com.duckduckgo.app.statistics.store.StatisticsDataStore
import com.duckduckgo.app.trackerdetection.blocklist.activeTdsFlag
import com.duckduckgo.app.trackerdetection.db.TdsMetadataDao
import com.duckduckgo.appbuildconfig.api.AppBuildConfig
import com.duckduckgo.brokensite.api.BrokenSite
Expand All @@ -40,6 +41,8 @@ import com.duckduckgo.common.utils.extensions.toSanitizedLanguageTag
import com.duckduckgo.di.scopes.AppScope
import com.duckduckgo.experiments.api.VariantManager
import com.duckduckgo.feature.toggles.api.FeatureToggle
import com.duckduckgo.feature.toggles.api.FeatureTogglesInventory
import com.duckduckgo.feature.toggles.api.Toggle
import com.duckduckgo.networkprotection.api.NetworkProtectionState
import com.duckduckgo.privacy.config.api.AmpLinks
import com.duckduckgo.privacy.config.api.ContentBlocking
Expand Down Expand Up @@ -75,6 +78,7 @@ class BrokenSiteSubmitter @Inject constructor(
private val networkProtectionState: NetworkProtectionState,
private val webViewVersionProvider: WebViewVersionProvider,
private val ampLinks: AmpLinks,
private val inventory: FeatureTogglesInventory,
) : BrokenSiteSender {

override fun submitBrokenSiteFeedback(brokenSite: BrokenSite) {
Expand All @@ -97,6 +101,8 @@ class BrokenSiteSubmitter @Inject constructor(
val vpnOn = runCatching { networkProtectionState.isRunning() }.getOrNull()
val locale = appBuildConfig.deviceLocale.toSanitizedLanguageTag()

val blockListToggle: Toggle? = inventory.activeTdsFlag()

val params = mutableMapOf(
CATEGORY_KEY to brokenSite.category.orEmpty(),
DESCRIPTION_KEY to brokenSite.description.orEmpty(),
Expand Down Expand Up @@ -131,6 +137,12 @@ class BrokenSiteSubmitter @Inject constructor(
params[REPORT_FLOW] = reportFlow.toStringValue()
}

blockListToggle?.let { toggle ->
toggle.getCohort()?.let { cohort ->
params[BLOCKLIST_EXPERIMENT] = "${toggle.featureName().name}_${cohort.name}"
}
}

val lastSentDay = brokenSiteLastSentReport.getLastSentDay(domain.orEmpty())
if (lastSentDay != null) {
params[LAST_SENT_DAY] = lastSentDay
Expand Down Expand Up @@ -201,6 +213,7 @@ class BrokenSiteSubmitter @Inject constructor(
private const val USER_REFRESH_COUNT = "userRefreshCount"
private const val OPENER_CONTEXT = "openerContext"
private const val JS_PERFORMANCE = "jsPerformance"
private const val BLOCKLIST_EXPERIMENT = "blockListExperiment"
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -266,6 +266,7 @@ import com.duckduckgo.common.utils.baseHost
import com.duckduckgo.common.utils.device.DeviceInfo
import com.duckduckgo.common.utils.extensions.asLocationPermissionOrigin
import com.duckduckgo.common.utils.isMobileSite
import com.duckduckgo.common.utils.plugins.PluginPoint
import com.duckduckgo.common.utils.toDesktopUri
import com.duckduckgo.di.scopes.FragmentScope
import com.duckduckgo.downloads.api.DownloadCommand
Expand All @@ -283,6 +284,8 @@ import com.duckduckgo.privacy.config.api.AmpLinks
import com.duckduckgo.privacy.config.api.ContentBlocking
import com.duckduckgo.privacy.config.api.Gpc
import com.duckduckgo.privacy.config.api.TrackingParameters
import com.duckduckgo.privacy.dashboard.api.PrivacyProtectionTogglePlugin
import com.duckduckgo.privacy.dashboard.api.PrivacyToggleOrigin
import com.duckduckgo.privacy.dashboard.impl.pixels.PrivacyDashboardPixels
import com.duckduckgo.privacyprotectionspopup.api.PrivacyProtectionsPopupExperimentExternalPixels
import com.duckduckgo.privacyprotectionspopup.api.PrivacyProtectionsPopupManager
Expand Down Expand Up @@ -427,6 +430,7 @@ class BrowserTabViewModel @Inject constructor(
private val refreshPixelSender: RefreshPixelSender,
private val changeOmnibarPositionFeature: ChangeOmnibarPositionFeature,
private val highlightsOnboardingExperimentManager: HighlightsOnboardingExperimentManager,
private val privacyProtectionTogglePlugin: PluginPoint<PrivacyProtectionTogglePlugin>,
) : WebViewClientListener,
EditSavedSiteListener,
DeleteBookmarkListener,
Expand Down Expand Up @@ -2468,6 +2472,9 @@ class BrowserTabViewModel @Inject constructor(
}
privacyProtectionsPopupExperimentExternalPixels.tryReportProtectionsToggledFromBrowserMenu(protectionsEnabled = false)
userAllowListRepository.addDomainToUserAllowList(domain)
privacyProtectionTogglePlugin.getPlugins().forEach {
it.onToggleOff(PrivacyToggleOrigin.MENU)
}
withContext(dispatchers.main()) {
command.value = ShowPrivacyProtectionDisabledConfirmation(domain)
browserViewState.value = currentBrowserViewState().copy(isPrivacyProtectionDisabled = true)
Expand All @@ -2486,6 +2493,9 @@ class BrowserTabViewModel @Inject constructor(
}
privacyProtectionsPopupExperimentExternalPixels.tryReportProtectionsToggledFromBrowserMenu(protectionsEnabled = true)
userAllowListRepository.removeDomainFromUserAllowList(domain)
privacyProtectionTogglePlugin.getPlugins().forEach {
it.onToggleOn(PrivacyToggleOrigin.MENU)
}
withContext(dispatchers.main()) {
command.value = ShowPrivacyProtectionEnabledConfirmation(domain)
browserViewState.value = currentBrowserViewState().copy(isPrivacyProtectionDisabled = false)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,9 @@ import com.duckduckgo.app.pixels.AppPixelName.RELOAD_TWICE_WITHIN_12_SECONDS
import com.duckduckgo.app.statistics.pixels.Pixel
import com.duckduckgo.app.statistics.pixels.Pixel.PixelParameter.LOADING_BAR_EXPERIMENT
import com.duckduckgo.app.statistics.pixels.Pixel.PixelType.Daily
import com.duckduckgo.app.trackerdetection.blocklist.BlockListPixelsPlugin
import com.duckduckgo.app.trackerdetection.blocklist.get2XRefresh
import com.duckduckgo.app.trackerdetection.blocklist.get3XRefresh
import com.duckduckgo.common.utils.CurrentTimeProvider
import com.duckduckgo.common.utils.DispatcherProvider
import com.duckduckgo.common.utils.extensions.toBinaryString
Expand All @@ -48,6 +51,7 @@ class DuckDuckGoRefreshPixelSender @Inject constructor(
private val dao: RefreshDao,
private val loadingBarExperimentManager: LoadingBarExperimentManager,
private val currentTimeProvider: CurrentTimeProvider,
private val blockListPixelsPlugin: BlockListPixelsPlugin,
@AppCoroutineScope private val appCoroutineScope: CoroutineScope,
private val dispatcherProvider: DispatcherProvider,
) : RefreshPixelSender {
Expand Down Expand Up @@ -108,9 +112,16 @@ class DuckDuckGoRefreshPixelSender @Inject constructor(

if (refreshes.count { it.timestamp >= twelveSecondsAgo } >= 2) {
pixel.fire(RELOAD_TWICE_WITHIN_12_SECONDS)
blockListPixelsPlugin.get2XRefresh()?.getPixelDefinitions()?.forEach {
pixel.fire(it.pixelName, it.params)
}
}
if (refreshes.size >= 3) {
pixel.fire(RELOAD_THREE_TIMES_WITHIN_20_SECONDS)

blockListPixelsPlugin.get3XRefresh()?.getPixelDefinitions()?.forEach {
pixel.fire(it.pixelName, it.params)
}
}
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
/*
* 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.app.trackerdetection.blocklist

import com.duckduckgo.anvil.annotations.ContributesRemoteFeature
import com.duckduckgo.app.di.AppCoroutineScope
import com.duckduckgo.app.trackerdetection.api.TrackerDataDownloader
import com.duckduckgo.app.trackerdetection.blocklist.BlockList.Companion.EXPERIMENT_PREFIX
import com.duckduckgo.app.trackerdetection.blocklist.ExperimentTestAA.Cohorts.CONTROL
import com.duckduckgo.common.utils.DispatcherProvider
import com.duckduckgo.di.scopes.AppScope
import com.duckduckgo.feature.toggles.api.ConversionWindow
import com.duckduckgo.feature.toggles.api.FeatureTogglesInventory
import com.duckduckgo.feature.toggles.api.MetricsPixel
import com.duckduckgo.feature.toggles.api.MetricsPixelPlugin
import com.duckduckgo.feature.toggles.api.Toggle
import com.duckduckgo.feature.toggles.api.Toggle.State.CohortName
import com.duckduckgo.privacy.config.api.PrivacyConfigCallbackPlugin
import com.squareup.anvil.annotations.ContributesMultibinding
import javax.inject.Inject
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch

@ContributesRemoteFeature(
scope = AppScope::class,
featureName = "blockList",
)
interface BlockList {
@Toggle.DefaultValue(false)
fun self(): Toggle

@Toggle.DefaultValue(false)
fun tdsNextExperimentBaseline(): Toggle

@Toggle.DefaultValue(false)
fun tdsNextExperimentBaselineBackup(): Toggle

@Toggle.DefaultValue(false)
fun tdsNextExperimentNov24(): Toggle

@Toggle.DefaultValue(false)
fun tdsNextExperimentDec24(): Toggle

@Toggle.DefaultValue(false)
fun tdsNextExperimentJan25(): Toggle

@Toggle.DefaultValue(false)
fun tdsNextExperimentFeb25(): Toggle

@Toggle.DefaultValue(false)
fun tdsNextExperimentMar25(): Toggle

enum class Cohorts(override val cohortName: String) : CohortName {
CONTROL("control"),
TREATMENT("treatment"),
}

companion object {
const val EXPERIMENT_PREFIX = "tds"
const val TREATMENT_URL = "treatmentUrl"
const val CONTROL_URL = "controlUrl"
const val NEXT_URL = "nextUrl"
}
}

@ContributesMultibinding(AppScope::class)
class BlockListPixelsPlugin @Inject constructor(private val inventory: FeatureTogglesInventory) : MetricsPixelPlugin {

override suspend fun getMetrics(): List<MetricsPixel> {
val activeToggle = inventory.activeTdsFlag() ?: return emptyList()

return listOf(
MetricsPixel(
metric = "2xRefresh",
value = "1",
toggle = activeToggle,
conversionWindow = (0..5).map { ConversionWindow(lowerWindow = 0, upperWindow = it) },
),
MetricsPixel(
metric = "3xRefresh",
value = "1",
toggle = activeToggle,
conversionWindow = (0..5).map { ConversionWindow(lowerWindow = 0, upperWindow = it) },
),
MetricsPixel(
metric = "privacyToggleUsed",
value = "1",
toggle = activeToggle,
conversionWindow = (0..5).map { ConversionWindow(lowerWindow = 0, upperWindow = it) },
),
)
}
}

@ContributesMultibinding(AppScope::class)
class BlockListPrivacyConfigCallbackPlugin @Inject constructor(
private val inventory: FeatureTogglesInventory,
private val trackerDataDownloader: TrackerDataDownloader,
@AppCoroutineScope private val coroutineScope: CoroutineScope,
private val experimentAA: ExperimentTestAA,
private val dispatcherProvider: DispatcherProvider,
) : PrivacyConfigCallbackPlugin {
override fun onPrivacyConfigDownloaded() {
coroutineScope.launch(dispatcherProvider.io()) {
experimentAA.experimentTestAA().isEnabled(CONTROL)
inventory.activeTdsFlag()?.let {
trackerDataDownloader.downloadTds()
}
}
}
}

internal suspend fun BlockListPixelsPlugin.get2XRefresh(): MetricsPixel? {
return this.getMetrics().firstOrNull { it.metric == "2xRefresh" }
}

suspend fun BlockListPixelsPlugin.get3XRefresh(): MetricsPixel? {
return this.getMetrics().firstOrNull { it.metric == "3xRefresh" }
}

suspend fun BlockListPixelsPlugin.getPrivacyToggleUsed(): MetricsPixel? {
return this.getMetrics().firstOrNull { it.metric == "privacyToggleUsed" }
}

suspend fun FeatureTogglesInventory.activeTdsFlag(): Toggle? {
return this.getAllTogglesForParent("blockList").firstOrNull {
it.featureName().name.startsWith(EXPERIMENT_PREFIX) && it.isEnabled()
}
}

@ContributesRemoteFeature(
scope = AppScope::class,
featureName = "experimentTest",
)
interface ExperimentTestAA {
@Toggle.DefaultValue(false)
fun self(): Toggle

@Toggle.DefaultValue(false)
fun experimentTestAA(): Toggle

enum class Cohorts(override val cohortName: String) : CohortName {
CONTROL("control"),
TREATMENT("treatment"),
}
}
Loading

0 comments on commit 725ff6d

Please sign in to comment.