Skip to content

Update 3x pixel-firing logic to more closely match broken site prompt #5862

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 18 commits into from
Apr 30, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -209,6 +209,7 @@ import com.duckduckgo.autofill.api.email.EmailManager
import com.duckduckgo.autofill.api.passwordgeneration.AutomaticSavedLoginsMonitor
import com.duckduckgo.autofill.impl.AutofillFireproofDialogSuppressor
import com.duckduckgo.brokensite.api.BrokenSitePrompt
import com.duckduckgo.brokensite.api.RefreshPattern
import com.duckduckgo.browser.api.UserBrowserProperties
import com.duckduckgo.browser.api.brokensite.BrokenSiteContext
import com.duckduckgo.common.test.CoroutineTestRule
Expand Down Expand Up @@ -2583,6 +2584,18 @@ class BrowserTabViewModelTest {
assertTrue(testee.ctaViewState.value!!.isErrorShowing)
}

@Test
fun whenCtaRefreshedGetUserRefreshesCalled() = runTest {
setBrowserShowing(true)
whenever(mockExtendedOnboardingFeatureToggles.noBrowserCtas()).thenReturn(mockDisabledToggle)
whenever(mockWidgetCapabilities.supportsAutomaticWidgetAdd).thenReturn(false)
whenever(mockWidgetCapabilities.hasInstalledWidgets).thenReturn(true)
val expectedRefreshPatterns = setOf(RefreshPattern.THRICE_IN_20_SECONDS)
whenever(mockBrokenSitePrompt.getUserRefreshPatterns()).thenReturn(expectedRefreshPatterns)
testee.refreshCta()
verify(mockBrokenSitePrompt).getUserRefreshPatterns()
}

@Test
fun whenCtaShownThenFirePixel() = runTest {
val cta = HomePanelCta.AddWidgetAuto
Expand Down Expand Up @@ -5772,6 +5785,17 @@ class BrowserTabViewModelTest {
verify(refreshPixelSender).sendCustomTabRefreshPixel()
}

@Test
fun whenRefreshCtaAndPatternsDetectedThenSendBreakageRefreshPixels() = runTest {
setBrowserShowing(true)
whenever(mockExtendedOnboardingFeatureToggles.noBrowserCtas()).thenReturn(mockDisabledToggle)
val refreshPatterns = setOf(RefreshPattern.TWICE_IN_12_SECONDS, RefreshPattern.THRICE_IN_20_SECONDS)
whenever(mockBrokenSitePrompt.getUserRefreshPatterns()).thenReturn(refreshPatterns)
testee.refreshCta()

verify(refreshPixelSender).onRefreshPatternDetected(refreshPatterns)
}

@Test
fun whenPageIsChangedWithWebViewErrorResponseThenPixelIsFired() = runTest {
testee.onReceivedError(BAD_URL, "example2.com")
Expand Down
178 changes: 139 additions & 39 deletions app/src/androidTest/java/com/duckduckgo/app/cta/ui/CtaViewModelTest.kt

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -279,6 +279,7 @@ import com.duckduckgo.autofill.api.email.EmailManager
import com.duckduckgo.autofill.api.passwordgeneration.AutomaticSavedLoginsMonitor
import com.duckduckgo.autofill.impl.AutofillFireproofDialogSuppressor
import com.duckduckgo.brokensite.api.BrokenSitePrompt
import com.duckduckgo.brokensite.api.RefreshPattern
import com.duckduckgo.browser.api.UserBrowserProperties
import com.duckduckgo.browser.api.brokensite.BrokenSiteData
import com.duckduckgo.browser.api.brokensite.BrokenSiteData.ReportFlow.MENU
Expand Down Expand Up @@ -481,7 +482,6 @@ class BrowserTabViewModel @Inject constructor(
private val appPersonalityFeature: AppPersonalityFeature,
private val userStageStore: UserStageStore,
private val privacyDashboardExternalPixelParams: PrivacyDashboardExternalPixelParams,

) : WebViewClientListener,
EditSavedSiteListener,
DeleteBookmarkListener,
Expand Down Expand Up @@ -2783,11 +2783,14 @@ class BrowserTabViewModel @Inject constructor(
val isBrowserShowing = currentBrowserViewState().browserShowing
val isErrorShowing = currentBrowserViewState().maliciousSiteBlocked
if (hasCtaBeenShownForCurrentPage.get() && isBrowserShowing) return null
val detectedRefreshPatterns = brokenSitePrompt.getUserRefreshPatterns()
handleBreakageRefreshPatterns(detectedRefreshPatterns)
val cta = withContext(dispatchers.io()) {
ctaViewModel.refreshCta(
dispatchers.io(),
isBrowserShowing && !isErrorShowing,
siteLiveData.value,
detectedRefreshPatterns,
)
}
val contextDaxDialogsShown = withContext(dispatchers.io()) {
Expand Down Expand Up @@ -4012,6 +4015,10 @@ class BrowserTabViewModel @Inject constructor(
refreshPixelSender.sendCustomTabRefreshPixel()
}

private fun handleBreakageRefreshPatterns(refreshPatterns: Set<RefreshPattern>) {
refreshPixelSender.onRefreshPatternDetected(refreshPatterns)
}

fun setBrowserBackground(lightModeEnabled: Boolean) {
command.value = SetBrowserBackground(getBackgroundResource(lightModeEnabled))
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,6 @@ import com.duckduckgo.app.browser.mediaplayback.store.MediaPlaybackDao
import com.duckduckgo.app.browser.mediaplayback.store.MediaPlaybackDatabase
import com.duckduckgo.app.browser.pageloadpixel.PageLoadedPixelDao
import com.duckduckgo.app.browser.pageloadpixel.firstpaint.PagePaintedPixelDao
import com.duckduckgo.app.browser.refreshpixels.RefreshDao
import com.duckduckgo.app.browser.session.WebViewSessionInMemoryStorage
import com.duckduckgo.app.browser.session.WebViewSessionStorage
import com.duckduckgo.app.browser.tabpreview.FileBasedWebViewPreviewGenerator
Expand Down Expand Up @@ -366,12 +365,6 @@ class BrowserModule {
return context.indonesiaNewTabSectionDataStore
}

@Provides
@SingleInstanceIn(AppScope::class)
fun provideRefreshDao(appDatabase: AppDatabase): RefreshDao {
return appDatabase.refreshDao()
}

@Provides
fun provideSiteErrorStringHandler(): StringSiteErrorHandler {
return StringSiteErrorHandlerImpl()
Expand Down

This file was deleted.

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -19,82 +19,71 @@ package com.duckduckgo.app.browser.refreshpixels
import com.duckduckgo.app.browser.customtabs.CustomTabPixelNames
import com.duckduckgo.app.di.AppCoroutineScope
import com.duckduckgo.app.pixels.AppPixelName
import com.duckduckgo.app.pixels.AppPixelName.RELOAD_THREE_TIMES_WITHIN_20_SECONDS
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.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.brokensite.api.RefreshPattern
import com.duckduckgo.common.utils.DispatcherProvider
import com.duckduckgo.di.scopes.AppScope
import com.squareup.anvil.annotations.ContributesBinding
import dagger.SingleInstanceIn
import javax.inject.Inject
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import timber.log.Timber

interface RefreshPixelSender {
fun sendMenuRefreshPixels()
fun sendCustomTabRefreshPixel()
fun sendPullToRefreshPixels()
fun onRefreshPatternDetected(patternsDetected: Set<RefreshPattern>)
}

@ContributesBinding(AppScope::class)
@SingleInstanceIn(AppScope::class)
class DuckDuckGoRefreshPixelSender @Inject constructor(
private val pixel: Pixel,
private val dao: RefreshDao,
private val currentTimeProvider: CurrentTimeProvider,
private val blockListPixelsPlugin: BlockListPixelsPlugin,
@AppCoroutineScope private val appCoroutineScope: CoroutineScope,
private val dispatcherProvider: DispatcherProvider,
) : RefreshPixelSender {

override fun sendMenuRefreshPixels() {
sendTimeBasedPixels()
pixel.fire(AppPixelName.MENU_ACTION_REFRESH_PRESSED)
pixel.fire(AppPixelName.REFRESH_ACTION_DAILY_PIXEL, type = Daily())
}

override fun sendPullToRefreshPixels() {
sendTimeBasedPixels()
pixel.fire(AppPixelName.BROWSER_PULL_TO_REFRESH)
pixel.fire(AppPixelName.REFRESH_ACTION_DAILY_PIXEL, type = Daily())
}

override fun sendCustomTabRefreshPixel() {
sendTimeBasedPixels()
pixel.fire(CustomTabPixelNames.CUSTOM_TABS_MENU_REFRESH)
}

private fun sendTimeBasedPixels() {
override fun onRefreshPatternDetected(patternsDetected: Set<RefreshPattern>) {
appCoroutineScope.launch(dispatcherProvider.io()) {
val now = currentTimeProvider.currentTimeMillis()
val twelveSecondsAgo = now - TWELVE_SECONDS
val twentySecondsAgo = now - TWENTY_SECONDS
patternsDetected.forEach { detectedPattern ->
when (detectedPattern) {
RefreshPattern.TWICE_IN_12_SECONDS -> {
blockListPixelsPlugin.get2XRefresh()?.getPixelDefinitions()?.forEach {
pixel.fire(it.pixelName, it.params)
}
pixel.fire(AppPixelName.RELOAD_TWICE_WITHIN_12_SECONDS)
}

val refreshes = dao.updateRecentRefreshes(twentySecondsAgo, now)

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)
RefreshPattern.THRICE_IN_20_SECONDS -> {
pixel.fire(AppPixelName.RELOAD_THREE_TIMES_WITHIN_20_SECONDS)
blockListPixelsPlugin.get3XRefresh()?.getPixelDefinitions()?.forEach {
pixel.fire(it.pixelName, it.params)
}
}
else -> Timber.w("Unknown refresh pattern: $detectedPattern, no pixels fired")
}
}
}
}

companion object {
const val TWENTY_SECONDS = 20000L
const val TWELVE_SECONDS = 12000L
}
}
9 changes: 6 additions & 3 deletions app/src/main/java/com/duckduckgo/app/cta/ui/CtaViewModel.kt
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ import com.duckduckgo.app.statistics.pixels.Pixel
import com.duckduckgo.app.tabs.model.TabRepository
import com.duckduckgo.app.widget.ui.WidgetCapabilities
import com.duckduckgo.brokensite.api.BrokenSitePrompt
import com.duckduckgo.brokensite.api.RefreshPattern
import com.duckduckgo.browser.api.UserBrowserProperties
import com.duckduckgo.common.utils.DispatcherProvider
import com.duckduckgo.di.scopes.AppScope
Expand Down Expand Up @@ -203,11 +204,12 @@ class CtaViewModel @Inject constructor(
dispatcher: CoroutineContext,
isBrowserShowing: Boolean,
site: Site? = null,
detectedRefreshPatterns: Set<RefreshPattern>,
): Cta? {
return withContext(dispatcher) {
markOnboardingAsCompletedIfRequiredCtasShown()
if (isBrowserShowing) {
getBrowserCta(site)
getBrowserCta(site, detectedRefreshPatterns)
} else {
getHomeCta()
}
Expand Down Expand Up @@ -306,7 +308,7 @@ class CtaViewModel @Inject constructor(
}

@WorkerThread
private suspend fun getBrowserCta(site: Site?): Cta? {
private suspend fun getBrowserCta(site: Site?, detectedRefreshPatterns: Set<RefreshPattern>): Cta? {
val nonNullSite = site ?: return null

val host = nonNullSite.domain
Expand All @@ -320,7 +322,8 @@ class CtaViewModel @Inject constructor(
}

if (areInContextDaxDialogsCompleted()) {
return if (brokenSitePrompt.shouldShowBrokenSitePrompt(nonNullSite.url)) {
return if (brokenSitePrompt.shouldShowBrokenSitePrompt(nonNullSite.url, detectedRefreshPatterns)
) {
BrokenSitePromptDialogCta()
} else {
null
Expand Down
14 changes: 8 additions & 6 deletions app/src/main/java/com/duckduckgo/app/global/db/AppDatabase.kt
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,6 @@ import com.duckduckgo.app.browser.pageloadpixel.PageLoadedPixelEntity
import com.duckduckgo.app.browser.pageloadpixel.firstpaint.PagePaintedPixelDao
import com.duckduckgo.app.browser.pageloadpixel.firstpaint.PagePaintedPixelEntity
import com.duckduckgo.app.browser.rating.db.*
import com.duckduckgo.app.browser.refreshpixels.RefreshDao
import com.duckduckgo.app.browser.refreshpixels.RefreshEntity
import com.duckduckgo.app.cta.db.DismissedCtaDao
import com.duckduckgo.app.cta.model.DismissedCta
import com.duckduckgo.app.fire.fireproofwebsite.data.FireproofWebsiteDao
Expand Down Expand Up @@ -75,7 +73,7 @@ import com.duckduckgo.savedsites.store.SavedSitesRelationsDao

@Database(
exportSchema = true,
version = 57,
version = 58,
entities = [
TdsTracker::class,
TdsEntity::class,
Expand Down Expand Up @@ -108,7 +106,6 @@ import com.duckduckgo.savedsites.store.SavedSitesRelationsDao
AuthCookieAllowedDomainEntity::class,
Entity::class,
Relation::class,
RefreshEntity::class,
ExperimentAppUsageEntity::class,
],
)
Expand Down Expand Up @@ -163,8 +160,6 @@ abstract class AppDatabase : RoomDatabase() {

abstract fun syncRelationsDao(): SavedSitesRelationsDao

abstract fun refreshDao(): RefreshDao

abstract fun experimentAppUsageDao(): ExperimentAppUsageDao
}

Expand Down Expand Up @@ -693,6 +688,12 @@ class MigrationsProvider(val context: Context, val settingsDataStore: SettingsDa
}
}

private val MIGRATION_57_TO_58: Migration = object : Migration(57, 58) {
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL("DROP TABLE IF EXISTS `refreshes`")
}
}

/**
* WARNING ⚠️
* This needs to happen because Room doesn't support UNIQUE (...) ON CONFLICT REPLACE when creating the bookmarks table.
Expand Down Expand Up @@ -775,6 +776,7 @@ class MigrationsProvider(val context: Context, val settingsDataStore: SettingsDa
MIGRATION_54_TO_55,
MIGRATION_55_TO_56,
MIGRATION_56_TO_57,
MIGRATION_57_TO_58,
)

@Deprecated(
Expand Down
Loading
Loading