Skip to content

Commit

Permalink
Add pixels for multiple refreshes (#5094)
Browse files Browse the repository at this point in the history
Task/Issue URL:
https://app.asana.com/0/72649045549333/1208470923077006/f

### Description
This adds two pixels:
- One for when the user refreshes twice within 12 seconds.
- And another when the user refreshes three times within 20 seconds.

It also refactors all the refresh pixel sending logic into
`RefreshPixelSender`, removing it from `BrowserTabFragment`.

### Steps to test this PR

Filter by `m_reload`

_Two refreshes in twelve seconds_
- [x] Refresh once
- [x] Verify no pixel sent
- [x] Refresh within 12 seconds
- [x] Verify `m_reload_twice_within_12_seconds` is sent

_Three refreshes in twenty seconds_
- [x] Refresh once
- [x] Refresh again immediately (`m_reload_twice_within_12_seconds` will
be sent)
- [x] Wait 12 seconds
- [x] Verify only `m_reload_three_times_within_20_seconds` is sent

_Three refreshes in twenty seconds and two refreshes in twelve seconds_
- [x] Refresh once
- [x] Refresh again immediately (`m_reload_twice_within_12_seconds` will
be sent)
- [x] Refresh again immediately
- [x] Verify `m_reload_twice_within_12_seconds` is sent
- [x] Verify `m_reload_three_times_within_20_seconds` is sent

_Two refreshes in twelve seconds after killing the app_
- [x] Refresh once
- [x] Verify no pixel sent
- [x] Kill the app
- [x] Refresh within 12 seconds
- [x] Verify `m_reload_twice_within_12_seconds` is sent
  • Loading branch information
joshliebe authored Oct 8, 2024
1 parent 7bf1c72 commit d818a4e
Show file tree
Hide file tree
Showing 10 changed files with 575 additions and 36 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,7 @@ import com.duckduckgo.app.browser.newtab.FavoritesQuickAccessAdapter.QuickAccess
import com.duckduckgo.app.browser.omnibar.OmnibarEntryConverter
import com.duckduckgo.app.browser.omnibar.model.OmnibarPosition.BOTTOM
import com.duckduckgo.app.browser.omnibar.model.OmnibarPosition.TOP
import com.duckduckgo.app.browser.refreshpixels.RefreshPixelSender
import com.duckduckgo.app.browser.remotemessage.RemoteMessagingModel
import com.duckduckgo.app.browser.session.WebViewSessionStorage
import com.duckduckgo.app.browser.viewstate.BrowserViewState
Expand Down Expand Up @@ -469,6 +470,8 @@ class BrowserTabViewModelTest {

private val subscriptions: Subscriptions = mock()

private val refreshPixelSender: RefreshPixelSender = mock()

private val privacyProtectionsPopupExperimentExternalPixels: PrivacyProtectionsPopupExperimentExternalPixels = mock {
runBlocking { whenever(mock.getPixelParams()).thenReturn(emptyMap()) }
}
Expand Down Expand Up @@ -639,6 +642,7 @@ class BrowserTabViewModelTest {
duckPlayer = mockDuckPlayer,
duckPlayerJSHelper = DuckPlayerJSHelper(mockDuckPlayer, mockAppBuildConfig, mockPixel, mockDuckDuckGoUrlDetector),
loadingBarExperimentManager = loadingBarExperimentManager,
refreshPixelSender = refreshPixelSender,
)

testee.loadData("abc", null, false, false)
Expand Down Expand Up @@ -5871,6 +5875,27 @@ class BrowserTabViewModelTest {
testee.omnibarViewState.removeObserver { observer(it) }
}

@Test
fun whenHandleMenuRefreshActionThenSendMenuRefreshPixels() {
testee.handleMenuRefreshAction()

verify(refreshPixelSender).sendMenuRefreshPixels()
}

@Test
fun whenHandlePullToRefreshActionThenSendPullToRefreshPixels() {
testee.handlePullToRefreshAction()

verify(refreshPixelSender).sendPullToRefreshPixels()
}

@Test
fun whenFireCustomTabRefreshPixelThenSendCustomTabRefreshPixel() {
testee.fireCustomTabRefreshPixel()

verify(refreshPixelSender).sendCustomTabRefreshPixel()
}

private fun aCredential(): LoginCredentials {
return LoginCredentials(domain = null, username = null, password = null)
}
Expand Down
38 changes: 3 additions & 35 deletions app/src/main/java/com/duckduckgo/app/browser/BrowserTabFragment.kt
Original file line number Diff line number Diff line change
Expand Up @@ -192,8 +192,6 @@ import com.duckduckgo.app.privatesearch.PrivateSearchScreenNoParams
import com.duckduckgo.app.settings.db.SettingsDataStore
import com.duckduckgo.app.statistics.pixels.Pixel
import com.duckduckgo.app.statistics.pixels.Pixel.PixelParameter.FIRE_BUTTON_STATE
import com.duckduckgo.app.statistics.pixels.Pixel.PixelParameter.LOADING_BAR_EXPERIMENT
import com.duckduckgo.app.statistics.pixels.Pixel.PixelType.Daily
import com.duckduckgo.app.tabs.ui.GridViewColumnCalculator
import com.duckduckgo.app.tabs.ui.TabSwitcherActivity
import com.duckduckgo.app.widget.AddWidgetLauncher
Expand Down Expand Up @@ -252,7 +250,6 @@ import com.duckduckgo.common.utils.KeyboardVisibilityUtil
import com.duckduckgo.common.utils.extensions.hideKeyboard
import com.duckduckgo.common.utils.extensions.html
import com.duckduckgo.common.utils.extensions.showKeyboard
import com.duckduckgo.common.utils.extensions.toBinaryString
import com.duckduckgo.common.utils.extensions.websiteFromGeoLocationsApiOrigin
import com.duckduckgo.common.utils.extractDomain
import com.duckduckgo.common.utils.playstore.PlayStoreUtils
Expand Down Expand Up @@ -966,23 +963,9 @@ class BrowserTabFragment :
onMenuItemClicked(refreshMenuItem) {
viewModel.onRefreshRequested(triggeredByUser = true)
if (isActiveCustomTab()) {
pixel.fire(CustomTabPixelNames.CUSTOM_TABS_MENU_REFRESH)
viewModel.fireCustomTabRefreshPixel()
} else {
// Loading Bar Experiment
if (loadingBarExperimentManager.isExperimentEnabled()) {
pixel.fire(
AppPixelName.MENU_ACTION_REFRESH_PRESSED.pixelName,
mapOf(LOADING_BAR_EXPERIMENT to loadingBarExperimentManager.variant.toBinaryString()),
)
pixel.fire(
AppPixelName.REFRESH_ACTION_DAILY_PIXEL.pixelName,
mapOf(LOADING_BAR_EXPERIMENT to loadingBarExperimentManager.variant.toBinaryString()),
type = Daily(),
)
} else {
pixel.fire(AppPixelName.MENU_ACTION_REFRESH_PRESSED.pixelName)
pixel.fire(AppPixelName.REFRESH_ACTION_DAILY_PIXEL.pixelName, type = Daily())
}
viewModel.handleMenuRefreshAction()
}
}
onMenuItemClicked(newTabMenuItem) {
Expand Down Expand Up @@ -2759,22 +2742,7 @@ class BrowserTabFragment :

binding.swipeRefreshContainer.setOnRefreshListener {
onRefreshRequested()

// Loading Bar Experiment
if (loadingBarExperimentManager.isExperimentEnabled()) {
pixel.fire(
AppPixelName.BROWSER_PULL_TO_REFRESH.pixelName,
mapOf(LOADING_BAR_EXPERIMENT to loadingBarExperimentManager.variant.toBinaryString()),
)
pixel.fire(
AppPixelName.REFRESH_ACTION_DAILY_PIXEL.pixelName,
mapOf(LOADING_BAR_EXPERIMENT to loadingBarExperimentManager.variant.toBinaryString()),
type = Daily(),
)
} else {
pixel.fire(AppPixelName.BROWSER_PULL_TO_REFRESH.pixelName)
pixel.fire(AppPixelName.REFRESH_ACTION_DAILY_PIXEL.pixelName, type = Daily())
}
viewModel.handlePullToRefreshAction()
}

binding.swipeRefreshContainer.setCanChildScrollUpCallback {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,7 @@ import com.duckduckgo.app.browser.omnibar.QueryOrigin.FromAutocomplete
import com.duckduckgo.app.browser.omnibar.model.OmnibarPosition
import com.duckduckgo.app.browser.omnibar.model.OmnibarPosition.BOTTOM
import com.duckduckgo.app.browser.omnibar.model.OmnibarPosition.TOP
import com.duckduckgo.app.browser.refreshpixels.RefreshPixelSender
import com.duckduckgo.app.browser.session.WebViewSessionStorage
import com.duckduckgo.app.browser.urlextraction.UrlExtractionListener
import com.duckduckgo.app.browser.viewstate.AccessibilityViewState
Expand Down Expand Up @@ -415,6 +416,7 @@ class BrowserTabViewModel @Inject constructor(
private val duckPlayer: DuckPlayer,
private val duckPlayerJSHelper: DuckPlayerJSHelper,
private val loadingBarExperimentManager: LoadingBarExperimentManager,
private val refreshPixelSender: RefreshPixelSender,
) : WebViewClientListener,
EditSavedSiteListener,
DeleteBookmarkListener,
Expand Down Expand Up @@ -3763,6 +3765,18 @@ class BrowserTabViewModel @Inject constructor(
newTabPixels.get().fireNewTabDisplayed()
}

fun handleMenuRefreshAction() {
refreshPixelSender.sendMenuRefreshPixels()
}

fun handlePullToRefreshAction() {
refreshPixelSender.sendPullToRefreshPixels()
}

fun fireCustomTabRefreshPixel() {
refreshPixelSender.sendCustomTabRefreshPixel()
}

companion object {
private const val FIXED_PROGRESS = 50

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ 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 @@ -365,6 +366,12 @@ class BrowserModule {
fun provideIndonesiaNewTabSectionDataStore(context: Context): DataStore<Preferences> {
return context.indonesiaNewTabSectionDataStore
}

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

@Qualifier
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
/*
* 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.browser.refreshpixels

import androidx.room.Dao
import androidx.room.Insert
import androidx.room.Query
import androidx.room.Transaction

@Dao
abstract class RefreshDao {

@Transaction
open fun updateRecentRefreshes(minTime: Long, currentTime: Long): List<RefreshEntity> {
insert(RefreshEntity(timestamp = currentTime))
deleteInvalidRefreshes(minTime, currentTime)
return all()
}

@Insert
abstract fun insert(entity: RefreshEntity)

@Query("DELETE FROM refreshes WHERE timestamp NOT BETWEEN :minTime AND :currentTime")
abstract fun deleteInvalidRefreshes(minTime: Long, currentTime: Long)

@Query("SELECT * FROM refreshes")
abstract fun all(): List<RefreshEntity>
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
/*
* 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.browser.refreshpixels

import androidx.room.Entity
import androidx.room.PrimaryKey

@Entity(tableName = "refreshes")
class RefreshEntity(
@PrimaryKey(autoGenerate = true) val id: Long = 0,
val timestamp: Long,
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
/*
* 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.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.PixelParameter.LOADING_BAR_EXPERIMENT
import com.duckduckgo.app.statistics.pixels.Pixel.PixelType.Daily
import com.duckduckgo.common.utils.CurrentTimeProvider
import com.duckduckgo.common.utils.DispatcherProvider
import com.duckduckgo.common.utils.extensions.toBinaryString
import com.duckduckgo.di.scopes.AppScope
import com.duckduckgo.experiments.api.loadingbarexperiment.LoadingBarExperimentManager
import com.squareup.anvil.annotations.ContributesBinding
import dagger.SingleInstanceIn
import javax.inject.Inject
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch

interface RefreshPixelSender {
fun sendMenuRefreshPixels()
fun sendCustomTabRefreshPixel()
fun sendPullToRefreshPixels()
}

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

override fun sendMenuRefreshPixels() {
sendTimeBasedPixels()

// Loading Bar Experiment
if (loadingBarExperimentManager.isExperimentEnabled()) {
pixel.fire(
AppPixelName.MENU_ACTION_REFRESH_PRESSED,
mapOf(LOADING_BAR_EXPERIMENT to loadingBarExperimentManager.variant.toBinaryString()),
)
pixel.fire(
AppPixelName.REFRESH_ACTION_DAILY_PIXEL,
mapOf(LOADING_BAR_EXPERIMENT to loadingBarExperimentManager.variant.toBinaryString()),
type = Daily(),
)
} else {
pixel.fire(AppPixelName.MENU_ACTION_REFRESH_PRESSED)
pixel.fire(AppPixelName.REFRESH_ACTION_DAILY_PIXEL, type = Daily())
}
}

override fun sendPullToRefreshPixels() {
sendTimeBasedPixels()

// Loading Bar Experiment
if (loadingBarExperimentManager.isExperimentEnabled()) {
pixel.fire(
AppPixelName.BROWSER_PULL_TO_REFRESH,
mapOf(LOADING_BAR_EXPERIMENT to loadingBarExperimentManager.variant.toBinaryString()),
)
pixel.fire(
AppPixelName.REFRESH_ACTION_DAILY_PIXEL,
mapOf(LOADING_BAR_EXPERIMENT to loadingBarExperimentManager.variant.toBinaryString()),
type = Daily(),
)
} else {
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() {
appCoroutineScope.launch(dispatcherProvider.io()) {
val now = currentTimeProvider.currentTimeMillis()
val twelveSecondsAgo = now - TWELVE_SECONDS
val twentySecondsAgo = now - TWENTY_SECONDS

val refreshes = dao.updateRecentRefreshes(twentySecondsAgo, now)

if (refreshes.count { it.timestamp >= twelveSecondsAgo } >= 2) {
pixel.fire(RELOAD_TWICE_WITHIN_12_SECONDS)
}
if (refreshes.size >= 3) {
pixel.fire(RELOAD_THREE_TIMES_WITHIN_20_SECONDS)
}
}
}

companion object {
const val TWENTY_SECONDS = 20000L
const val TWELVE_SECONDS = 12000L
}
}
Loading

0 comments on commit d818a4e

Please sign in to comment.