Skip to content

Commit c565825

Browse files
authored
Update 3x pixel-firing logic to more closely match broken site prompt (#5862)
Task/Issue URL: https://app.asana.com/0/72649045549333/1209712075249435/f ### Description We decided in https://app.asana.com/0/1205142657210376/1209698956036733 that we would prefer the separate 3x pixel to fire for distinct 3-refresh groupings in the chosen time period as opposed to 3 refreshes that fall into a sliding window of time to more closely match the breakage prompt trigger. ### Steps to test this PR NB: Logic is currently patched to delay 7 seconds between accepted prompts & stop prompting with 3+ dismissals within the past 30 min. _Feature 1_ - [x] Go to a site - [x] Refresh 3 times - [x] Verify that RELOAD-2X and RELOAD-3X pixels fire - [x] Dismiss prompt - [x] Refresh 3 times, pause for at least 20 seconds, then refresh 3 times again - [x] Verify that RELOAD-2X and RELOAD-3X fire twice
1 parent 2e7d7a3 commit c565825

File tree

21 files changed

+623
-489
lines changed

21 files changed

+623
-489
lines changed

app/src/androidTest/java/com/duckduckgo/app/browser/BrowserTabViewModelTest.kt

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -209,6 +209,7 @@ import com.duckduckgo.autofill.api.email.EmailManager
209209
import com.duckduckgo.autofill.api.passwordgeneration.AutomaticSavedLoginsMonitor
210210
import com.duckduckgo.autofill.impl.AutofillFireproofDialogSuppressor
211211
import com.duckduckgo.brokensite.api.BrokenSitePrompt
212+
import com.duckduckgo.brokensite.api.RefreshPattern
212213
import com.duckduckgo.browser.api.UserBrowserProperties
213214
import com.duckduckgo.browser.api.brokensite.BrokenSiteContext
214215
import com.duckduckgo.common.test.CoroutineTestRule
@@ -2583,6 +2584,18 @@ class BrowserTabViewModelTest {
25832584
assertTrue(testee.ctaViewState.value!!.isErrorShowing)
25842585
}
25852586

2587+
@Test
2588+
fun whenCtaRefreshedGetUserRefreshesCalled() = runTest {
2589+
setBrowserShowing(true)
2590+
whenever(mockExtendedOnboardingFeatureToggles.noBrowserCtas()).thenReturn(mockDisabledToggle)
2591+
whenever(mockWidgetCapabilities.supportsAutomaticWidgetAdd).thenReturn(false)
2592+
whenever(mockWidgetCapabilities.hasInstalledWidgets).thenReturn(true)
2593+
val expectedRefreshPatterns = setOf(RefreshPattern.THRICE_IN_20_SECONDS)
2594+
whenever(mockBrokenSitePrompt.getUserRefreshPatterns()).thenReturn(expectedRefreshPatterns)
2595+
testee.refreshCta()
2596+
verify(mockBrokenSitePrompt).getUserRefreshPatterns()
2597+
}
2598+
25862599
@Test
25872600
fun whenCtaShownThenFirePixel() = runTest {
25882601
val cta = HomePanelCta.AddWidgetAuto
@@ -5772,6 +5785,17 @@ class BrowserTabViewModelTest {
57725785
verify(refreshPixelSender).sendCustomTabRefreshPixel()
57735786
}
57745787

5788+
@Test
5789+
fun whenRefreshCtaAndPatternsDetectedThenSendBreakageRefreshPixels() = runTest {
5790+
setBrowserShowing(true)
5791+
whenever(mockExtendedOnboardingFeatureToggles.noBrowserCtas()).thenReturn(mockDisabledToggle)
5792+
val refreshPatterns = setOf(RefreshPattern.TWICE_IN_12_SECONDS, RefreshPattern.THRICE_IN_20_SECONDS)
5793+
whenever(mockBrokenSitePrompt.getUserRefreshPatterns()).thenReturn(refreshPatterns)
5794+
testee.refreshCta()
5795+
5796+
verify(refreshPixelSender).onRefreshPatternDetected(refreshPatterns)
5797+
}
5798+
57755799
@Test
57765800
fun whenPageIsChangedWithWebViewErrorResponseThenPixelIsFired() = runTest {
57775801
testee.onReceivedError(BAD_URL, "example2.com")

app/src/androidTest/java/com/duckduckgo/app/cta/ui/CtaViewModelTest.kt

Lines changed: 139 additions & 39 deletions
Large diffs are not rendered by default.

app/src/main/java/com/duckduckgo/app/browser/BrowserTabViewModel.kt

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -279,6 +279,7 @@ import com.duckduckgo.autofill.api.email.EmailManager
279279
import com.duckduckgo.autofill.api.passwordgeneration.AutomaticSavedLoginsMonitor
280280
import com.duckduckgo.autofill.impl.AutofillFireproofDialogSuppressor
281281
import com.duckduckgo.brokensite.api.BrokenSitePrompt
282+
import com.duckduckgo.brokensite.api.RefreshPattern
282283
import com.duckduckgo.browser.api.UserBrowserProperties
283284
import com.duckduckgo.browser.api.brokensite.BrokenSiteData
284285
import com.duckduckgo.browser.api.brokensite.BrokenSiteData.ReportFlow.MENU
@@ -482,7 +483,6 @@ class BrowserTabViewModel @Inject constructor(
482483
private val appPersonalityFeature: AppPersonalityFeature,
483484
private val userStageStore: UserStageStore,
484485
private val privacyDashboardExternalPixelParams: PrivacyDashboardExternalPixelParams,
485-
486486
) : WebViewClientListener,
487487
EditSavedSiteListener,
488488
DeleteBookmarkListener,
@@ -2784,11 +2784,14 @@ class BrowserTabViewModel @Inject constructor(
27842784
val isBrowserShowing = currentBrowserViewState().browserShowing
27852785
val isErrorShowing = currentBrowserViewState().maliciousSiteBlocked
27862786
if (hasCtaBeenShownForCurrentPage.get() && isBrowserShowing) return null
2787+
val detectedRefreshPatterns = brokenSitePrompt.getUserRefreshPatterns()
2788+
handleBreakageRefreshPatterns(detectedRefreshPatterns)
27872789
val cta = withContext(dispatchers.io()) {
27882790
ctaViewModel.refreshCta(
27892791
dispatchers.io(),
27902792
isBrowserShowing && !isErrorShowing,
27912793
siteLiveData.value,
2794+
detectedRefreshPatterns,
27922795
)
27932796
}
27942797
val contextDaxDialogsShown = withContext(dispatchers.io()) {
@@ -4014,6 +4017,10 @@ class BrowserTabViewModel @Inject constructor(
40144017
refreshPixelSender.sendCustomTabRefreshPixel()
40154018
}
40164019

4020+
private fun handleBreakageRefreshPatterns(refreshPatterns: Set<RefreshPattern>) {
4021+
refreshPixelSender.onRefreshPatternDetected(refreshPatterns)
4022+
}
4023+
40174024
fun setBrowserBackground(lightModeEnabled: Boolean) {
40184025
command.value = SetBrowserBackground(getBackgroundResource(lightModeEnabled))
40194026
}

app/src/main/java/com/duckduckgo/app/browser/di/BrowserModule.kt

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,6 @@ import com.duckduckgo.app.browser.mediaplayback.store.MediaPlaybackDao
5050
import com.duckduckgo.app.browser.mediaplayback.store.MediaPlaybackDatabase
5151
import com.duckduckgo.app.browser.pageloadpixel.PageLoadedPixelDao
5252
import com.duckduckgo.app.browser.pageloadpixel.firstpaint.PagePaintedPixelDao
53-
import com.duckduckgo.app.browser.refreshpixels.RefreshDao
5453
import com.duckduckgo.app.browser.session.WebViewSessionInMemoryStorage
5554
import com.duckduckgo.app.browser.session.WebViewSessionStorage
5655
import com.duckduckgo.app.browser.tabpreview.FileBasedWebViewPreviewGenerator
@@ -366,12 +365,6 @@ class BrowserModule {
366365
return context.indonesiaNewTabSectionDataStore
367366
}
368367

369-
@Provides
370-
@SingleInstanceIn(AppScope::class)
371-
fun provideRefreshDao(appDatabase: AppDatabase): RefreshDao {
372-
return appDatabase.refreshDao()
373-
}
374-
375368
@Provides
376369
fun provideSiteErrorStringHandler(): StringSiteErrorHandler {
377370
return StringSiteErrorHandlerImpl()

app/src/main/java/com/duckduckgo/app/browser/refreshpixels/RefreshDao.kt

Lines changed: 0 additions & 42 deletions
This file was deleted.

app/src/main/java/com/duckduckgo/app/browser/refreshpixels/RefreshEntity.kt

Lines changed: 0 additions & 26 deletions
This file was deleted.

app/src/main/java/com/duckduckgo/app/browser/refreshpixels/RefreshPixelSender.kt

Lines changed: 19 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -19,82 +19,71 @@ package com.duckduckgo.app.browser.refreshpixels
1919
import com.duckduckgo.app.browser.customtabs.CustomTabPixelNames
2020
import com.duckduckgo.app.di.AppCoroutineScope
2121
import com.duckduckgo.app.pixels.AppPixelName
22-
import com.duckduckgo.app.pixels.AppPixelName.RELOAD_THREE_TIMES_WITHIN_20_SECONDS
23-
import com.duckduckgo.app.pixels.AppPixelName.RELOAD_TWICE_WITHIN_12_SECONDS
2422
import com.duckduckgo.app.statistics.pixels.Pixel
2523
import com.duckduckgo.app.statistics.pixels.Pixel.PixelType.Daily
2624
import com.duckduckgo.app.trackerdetection.blocklist.BlockListPixelsPlugin
2725
import com.duckduckgo.app.trackerdetection.blocklist.get2XRefresh
2826
import com.duckduckgo.app.trackerdetection.blocklist.get3XRefresh
29-
import com.duckduckgo.common.utils.CurrentTimeProvider
27+
import com.duckduckgo.brokensite.api.RefreshPattern
3028
import com.duckduckgo.common.utils.DispatcherProvider
3129
import com.duckduckgo.di.scopes.AppScope
3230
import com.squareup.anvil.annotations.ContributesBinding
3331
import dagger.SingleInstanceIn
3432
import javax.inject.Inject
3533
import kotlinx.coroutines.CoroutineScope
3634
import kotlinx.coroutines.launch
35+
import timber.log.Timber
3736

3837
interface RefreshPixelSender {
3938
fun sendMenuRefreshPixels()
4039
fun sendCustomTabRefreshPixel()
4140
fun sendPullToRefreshPixels()
41+
fun onRefreshPatternDetected(patternsDetected: Set<RefreshPattern>)
4242
}
4343

4444
@ContributesBinding(AppScope::class)
4545
@SingleInstanceIn(AppScope::class)
4646
class DuckDuckGoRefreshPixelSender @Inject constructor(
4747
private val pixel: Pixel,
48-
private val dao: RefreshDao,
49-
private val currentTimeProvider: CurrentTimeProvider,
5048
private val blockListPixelsPlugin: BlockListPixelsPlugin,
5149
@AppCoroutineScope private val appCoroutineScope: CoroutineScope,
5250
private val dispatcherProvider: DispatcherProvider,
5351
) : RefreshPixelSender {
5452

5553
override fun sendMenuRefreshPixels() {
56-
sendTimeBasedPixels()
5754
pixel.fire(AppPixelName.MENU_ACTION_REFRESH_PRESSED)
5855
pixel.fire(AppPixelName.REFRESH_ACTION_DAILY_PIXEL, type = Daily())
5956
}
6057

6158
override fun sendPullToRefreshPixels() {
62-
sendTimeBasedPixels()
6359
pixel.fire(AppPixelName.BROWSER_PULL_TO_REFRESH)
6460
pixel.fire(AppPixelName.REFRESH_ACTION_DAILY_PIXEL, type = Daily())
6561
}
6662

6763
override fun sendCustomTabRefreshPixel() {
68-
sendTimeBasedPixels()
6964
pixel.fire(CustomTabPixelNames.CUSTOM_TABS_MENU_REFRESH)
7065
}
7166

72-
private fun sendTimeBasedPixels() {
67+
override fun onRefreshPatternDetected(patternsDetected: Set<RefreshPattern>) {
7368
appCoroutineScope.launch(dispatcherProvider.io()) {
74-
val now = currentTimeProvider.currentTimeMillis()
75-
val twelveSecondsAgo = now - TWELVE_SECONDS
76-
val twentySecondsAgo = now - TWENTY_SECONDS
69+
patternsDetected.forEach { detectedPattern ->
70+
when (detectedPattern) {
71+
RefreshPattern.TWICE_IN_12_SECONDS -> {
72+
blockListPixelsPlugin.get2XRefresh()?.getPixelDefinitions()?.forEach {
73+
pixel.fire(it.pixelName, it.params)
74+
}
75+
pixel.fire(AppPixelName.RELOAD_TWICE_WITHIN_12_SECONDS)
76+
}
7777

78-
val refreshes = dao.updateRecentRefreshes(twentySecondsAgo, now)
79-
80-
if (refreshes.count { it.timestamp >= twelveSecondsAgo } >= 2) {
81-
pixel.fire(RELOAD_TWICE_WITHIN_12_SECONDS)
82-
blockListPixelsPlugin.get2XRefresh()?.getPixelDefinitions()?.forEach {
83-
pixel.fire(it.pixelName, it.params)
84-
}
85-
}
86-
if (refreshes.size >= 3) {
87-
pixel.fire(RELOAD_THREE_TIMES_WITHIN_20_SECONDS)
88-
89-
blockListPixelsPlugin.get3XRefresh()?.getPixelDefinitions()?.forEach {
90-
pixel.fire(it.pixelName, it.params)
78+
RefreshPattern.THRICE_IN_20_SECONDS -> {
79+
pixel.fire(AppPixelName.RELOAD_THREE_TIMES_WITHIN_20_SECONDS)
80+
blockListPixelsPlugin.get3XRefresh()?.getPixelDefinitions()?.forEach {
81+
pixel.fire(it.pixelName, it.params)
82+
}
83+
}
84+
else -> Timber.w("Unknown refresh pattern: $detectedPattern, no pixels fired")
9185
}
9286
}
9387
}
9488
}
95-
96-
companion object {
97-
const val TWENTY_SECONDS = 20000L
98-
const val TWELVE_SECONDS = 12000L
99-
}
10089
}

app/src/main/java/com/duckduckgo/app/cta/ui/CtaViewModel.kt

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ import com.duckduckgo.app.statistics.pixels.Pixel
4242
import com.duckduckgo.app.tabs.model.TabRepository
4343
import com.duckduckgo.app.widget.ui.WidgetCapabilities
4444
import com.duckduckgo.brokensite.api.BrokenSitePrompt
45+
import com.duckduckgo.brokensite.api.RefreshPattern
4546
import com.duckduckgo.browser.api.UserBrowserProperties
4647
import com.duckduckgo.common.utils.DispatcherProvider
4748
import com.duckduckgo.di.scopes.AppScope
@@ -203,11 +204,12 @@ class CtaViewModel @Inject constructor(
203204
dispatcher: CoroutineContext,
204205
isBrowserShowing: Boolean,
205206
site: Site? = null,
207+
detectedRefreshPatterns: Set<RefreshPattern>,
206208
): Cta? {
207209
return withContext(dispatcher) {
208210
markOnboardingAsCompletedIfRequiredCtasShown()
209211
if (isBrowserShowing) {
210-
getBrowserCta(site)
212+
getBrowserCta(site, detectedRefreshPatterns)
211213
} else {
212214
getHomeCta()
213215
}
@@ -306,7 +308,7 @@ class CtaViewModel @Inject constructor(
306308
}
307309

308310
@WorkerThread
309-
private suspend fun getBrowserCta(site: Site?): Cta? {
311+
private suspend fun getBrowserCta(site: Site?, detectedRefreshPatterns: Set<RefreshPattern>): Cta? {
310312
val nonNullSite = site ?: return null
311313

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

322324
if (areInContextDaxDialogsCompleted()) {
323-
return if (brokenSitePrompt.shouldShowBrokenSitePrompt(nonNullSite.url)) {
325+
return if (brokenSitePrompt.shouldShowBrokenSitePrompt(nonNullSite.url, detectedRefreshPatterns)
326+
) {
324327
BrokenSitePromptDialogCta()
325328
} else {
326329
null

app/src/main/java/com/duckduckgo/app/global/db/AppDatabase.kt

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -33,8 +33,6 @@ import com.duckduckgo.app.browser.pageloadpixel.PageLoadedPixelEntity
3333
import com.duckduckgo.app.browser.pageloadpixel.firstpaint.PagePaintedPixelDao
3434
import com.duckduckgo.app.browser.pageloadpixel.firstpaint.PagePaintedPixelEntity
3535
import com.duckduckgo.app.browser.rating.db.*
36-
import com.duckduckgo.app.browser.refreshpixels.RefreshDao
37-
import com.duckduckgo.app.browser.refreshpixels.RefreshEntity
3836
import com.duckduckgo.app.cta.db.DismissedCtaDao
3937
import com.duckduckgo.app.cta.model.DismissedCta
4038
import com.duckduckgo.app.fire.fireproofwebsite.data.FireproofWebsiteDao
@@ -75,7 +73,7 @@ import com.duckduckgo.savedsites.store.SavedSitesRelationsDao
7573

7674
@Database(
7775
exportSchema = true,
78-
version = 57,
76+
version = 58,
7977
entities = [
8078
TdsTracker::class,
8179
TdsEntity::class,
@@ -108,7 +106,6 @@ import com.duckduckgo.savedsites.store.SavedSitesRelationsDao
108106
AuthCookieAllowedDomainEntity::class,
109107
Entity::class,
110108
Relation::class,
111-
RefreshEntity::class,
112109
ExperimentAppUsageEntity::class,
113110
],
114111
)
@@ -163,8 +160,6 @@ abstract class AppDatabase : RoomDatabase() {
163160

164161
abstract fun syncRelationsDao(): SavedSitesRelationsDao
165162

166-
abstract fun refreshDao(): RefreshDao
167-
168163
abstract fun experimentAppUsageDao(): ExperimentAppUsageDao
169164
}
170165

@@ -693,6 +688,12 @@ class MigrationsProvider(val context: Context, val settingsDataStore: SettingsDa
693688
}
694689
}
695690

691+
private val MIGRATION_57_TO_58: Migration = object : Migration(57, 58) {
692+
override fun migrate(database: SupportSQLiteDatabase) {
693+
database.execSQL("DROP TABLE IF EXISTS `refreshes`")
694+
}
695+
}
696+
696697
/**
697698
* WARNING ⚠️
698699
* This needs to happen because Room doesn't support UNIQUE (...) ON CONFLICT REPLACE when creating the bookmarks table.
@@ -775,6 +776,7 @@ class MigrationsProvider(val context: Context, val settingsDataStore: SettingsDa
775776
MIGRATION_54_TO_55,
776777
MIGRATION_55_TO_56,
777778
MIGRATION_56_TO_57,
779+
MIGRATION_57_TO_58,
778780
)
779781

780782
@Deprecated(

0 commit comments

Comments
 (0)