Skip to content
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

Pixel: Add tab activity stats to a daily pixel #5278

Merged
merged 22 commits into from
Nov 19, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
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 @@ -175,6 +175,7 @@ import com.duckduckgo.app.statistics.pixels.Pixel.PixelValues.DAX_FIRE_DIALOG_CT
import com.duckduckgo.app.surrogates.SurrogateResponse
import com.duckduckgo.app.tabs.model.TabEntity
import com.duckduckgo.app.tabs.model.TabRepository
import com.duckduckgo.app.tabs.store.TabStatsBucketing
import com.duckduckgo.app.trackerdetection.EntityLookup
import com.duckduckgo.app.trackerdetection.model.TrackerStatus
import com.duckduckgo.app.trackerdetection.model.TrackerType
Expand Down Expand Up @@ -494,6 +495,7 @@ class BrowserTabViewModelTest {
private val mockAutocompleteTabsFeature: AutocompleteTabsFeature = mock()
private val fakeCustomHeadersPlugin = FakeCustomHeadersProvider(emptyMap())
private val mockBrokenSitePrompt: BrokenSitePrompt = mock()
private val mockTabStatsBucketing: TabStatsBucketing = mock()

@Before
fun before() = runTest {
Expand Down Expand Up @@ -661,6 +663,7 @@ class BrowserTabViewModelTest {
showOnAppLaunchOptionHandler = mockShowOnAppLaunchHandler,
customHeadersProvider = fakeCustomHeadersPlugin,
brokenSitePrompt = mockBrokenSitePrompt,
tabStatsBucketing = mockTabStatsBucketing,
)

testee.loadData("abc", null, false, false)
Expand Down Expand Up @@ -5442,12 +5445,32 @@ class BrowserTabViewModelTest {
}

@Test
fun whenUserLaunchingTabSwitcherThenLaunchTabSwitcherCommandSentAndPixelFired() {
fun whenUserLaunchingTabSwitcherThenLaunchTabSwitcherCommandSentAndPixelFired() = runTest {
val tabCount = "61-80"
val active7d = "21+"
val inactive1w = "11-20"
val inactive2w = "6-10"
val inactive3w = "0"

whenever(mockTabStatsBucketing.getNumberOfOpenTabs()).thenReturn(tabCount)
whenever(mockTabStatsBucketing.getTabsActiveLastWeek()).thenReturn(active7d)
whenever(mockTabStatsBucketing.getTabsActiveOneWeekAgo()).thenReturn(inactive1w)
whenever(mockTabStatsBucketing.getTabsActiveTwoWeeksAgo()).thenReturn(inactive2w)
whenever(mockTabStatsBucketing.getTabsActiveMoreThanThreeWeeksAgo()).thenReturn(inactive3w)

val params = mapOf(
PixelParameter.TAB_COUNT to tabCount,
PixelParameter.TAB_ACTIVE_7D to active7d,
PixelParameter.TAB_INACTIVE_1W to inactive1w,
PixelParameter.TAB_INACTIVE_2W to inactive2w,
PixelParameter.TAB_INACTIVE_3W to inactive3w,
)

testee.userLaunchingTabSwitcher()

assertCommandIssued<Command.LaunchTabSwitcher>()
verify(mockPixel).fire(AppPixelName.TAB_MANAGER_CLICKED)
verify(mockPixel).fire(AppPixelName.TAB_MANAGER_CLICKED_DAILY, emptyMap(), emptyMap(), Daily())
verify(mockPixel).fire(AppPixelName.TAB_MANAGER_CLICKED_DAILY, params, emptyMap(), Daily())
}

@Test
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@ import com.duckduckgo.common.test.CoroutineTestRule
import com.duckduckgo.common.test.InstantSchedulersRule
import com.duckduckgo.duckplayer.api.DuckPlayer
import com.duckduckgo.privacy.config.api.ContentBlocking
import java.time.LocalDateTime
import java.time.ZoneOffset
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.consumeAsFlow
import kotlinx.coroutines.launch
Expand Down Expand Up @@ -420,6 +422,146 @@ class TabDataRepositoryTest {
job.cancel()
}

@Test
fun getOpenTabCountReturnsCorrectCount() = runTest {
// Arrange: Add some tabs to the repository
whenever(mockDao.tabs()).thenReturn(
listOf(
TabEntity(tabId = "tab1"),
TabEntity(tabId = "tab2"),
TabEntity(tabId = "tab3"),
),
)
val testee = tabDataRepository()

val openTabCount = testee.getOpenTabCount()

// Assert: Verify the count is correct
assertEquals(3, openTabCount)
}

@Test
fun getActiveTabCountReturnsZeroWhenNoTabs() = runTest {
// Arrange: No tabs in the repository
whenever(mockDao.tabs()).thenReturn(emptyList())
val testee = tabDataRepository()

val inactiveTabCount = testee.countTabsAccessedWithinRange(0, 7)

// Assert: Verify the count is zero
assertEquals(0, inactiveTabCount)
}

@Test
fun getActiveTabCountReturnsZeroWhenNullTabs() = runTest {
// Arrange: Only null tabs in the repository
val tab1 = TabEntity(tabId = "tab1")
whenever(mockDao.tabs()).thenReturn(listOf(tab1))
val testee = tabDataRepository()

val inactiveTabCount = testee.countTabsAccessedWithinRange(0, 7)

// Assert: Verify the count is zero
assertEquals(0, inactiveTabCount)
}

@Test
fun getActiveTabCountReturnsCorrectCountWhenTabsYoungerThanSpecifiedDay() = runTest {
// Arrange: No tabs in the repository
val now = LocalDateTime.now(ZoneOffset.UTC)
val tab1 = TabEntity(tabId = "tab1", lastAccessTime = now.minusDays(6))
val tab2 = TabEntity(tabId = "tab2", lastAccessTime = now.minusDays(8))
val tab3 = TabEntity(tabId = "tab3", lastAccessTime = now.minusDays(10))
val tab4 = TabEntity(tabId = "tab4")
whenever(mockDao.tabs()).thenReturn(listOf(tab1, tab2, tab3, tab4))
val testee = tabDataRepository()

val inactiveTabCount = testee.countTabsAccessedWithinRange(0, 9)

// Assert: Verify the count is 2
assertEquals(2, inactiveTabCount)
}

@Test
fun getInactiveTabCountReturnsZeroWhenNoTabs() = runTest {
// Arrange: No tabs in the repository
whenever(mockDao.tabs()).thenReturn(emptyList())
val testee = tabDataRepository()

val inactiveTabCount = testee.countTabsAccessedWithinRange(7, 12)

// Assert: Verify the count is zero
assertEquals(0, inactiveTabCount)
}

@Test
fun getInactiveTabCountReturnsCorrectCountWhenAllTabsOlderThanSpecifiedDay() = runTest {
// Arrange: Add some tabs with different last access times
val now = LocalDateTime.now(ZoneOffset.UTC)
val tab1 = TabEntity(tabId = "tab1", lastAccessTime = now.minusDays(8))
val tab2 = TabEntity(tabId = "tab2", lastAccessTime = now.minusDays(10))
val tab3 = TabEntity(tabId = "tab3", lastAccessTime = now.minusDays(9))
val tab4 = TabEntity(tabId = "tab4")
whenever(mockDao.tabs()).thenReturn(listOf(tab1, tab2, tab3, tab4))
val testee = tabDataRepository()

val inactiveTabCount = testee.countTabsAccessedWithinRange(9)

// Assert: Verify the count is correct
assertEquals(2, inactiveTabCount)
}

@Test
fun getInactiveTabCountReturnsCorrectCountWhenAllTabsInactiveWithinRange() = runTest {
// Arrange: Add some tabs with different last access times
val now = LocalDateTime.now(ZoneOffset.UTC)
val tab1 = TabEntity(tabId = "tab1", lastAccessTime = now.minusDays(8))
val tab2 = TabEntity(tabId = "tab2", lastAccessTime = now.minusDays(10))
val tab3 = TabEntity(tabId = "tab3", lastAccessTime = now.minusDays(9))
val tab4 = TabEntity(tabId = "tab4")
whenever(mockDao.tabs()).thenReturn(listOf(tab1, tab2, tab3, tab4))
val testee = tabDataRepository()

val inactiveTabCount = testee.countTabsAccessedWithinRange(7, 12)

// Assert: Verify the count is correct
assertEquals(3, inactiveTabCount)
}

@Test
fun getInactiveTabCountReturnsZeroWhenNoTabsInactiveWithinRange() = runTest {
// Arrange: Add some tabs with different last access times
val now = LocalDateTime.now(ZoneOffset.UTC)
val tab1 = TabEntity(tabId = "tab1", lastAccessTime = now.minusDays(5))
val tab2 = TabEntity(tabId = "tab2", lastAccessTime = now.minusDays(6))
val tab3 = TabEntity(tabId = "tab3", lastAccessTime = now.minusDays(13))
val tab4 = TabEntity(tabId = "tab4")
whenever(mockDao.tabs()).thenReturn(listOf(tab1, tab2, tab3, tab4))
val testee = tabDataRepository()

val inactiveTabCount = testee.countTabsAccessedWithinRange(7, 12)

// Assert: Verify the count is zero
assertEquals(0, inactiveTabCount)
}

@Test
fun getInactiveTabCountReturnsCorrectCountWhenSomeTabsInactiveWithinRange() = runTest {
// Arrange: Add some tabs with different last access times
val now = LocalDateTime.now(ZoneOffset.UTC)
val tab1 = TabEntity(tabId = "tab1", lastAccessTime = now.minusDays(5))
val tab2 = TabEntity(tabId = "tab2", lastAccessTime = now.minusDays(10))
val tab3 = TabEntity(tabId = "tab3", lastAccessTime = now.minusDays(15))
val tab4 = TabEntity(tabId = "tab4")
whenever(mockDao.tabs()).thenReturn(listOf(tab1, tab2, tab3, tab4))
val testee = tabDataRepository()

val inactiveTabCount = testee.countTabsAccessedWithinRange(7, 12)

// Assert: Verify the count is correct
assertEquals(1, inactiveTabCount)
}

private fun tabDataRepository(
dao: TabsDao = mockDatabase(),
entityLookup: EntityLookup = mock(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -272,6 +272,8 @@ open class BrowserActivity : DuckDuckGoActivity() {

lastActiveTabs.add(tab.tabId)

viewModel.onTabActivated(tab.tabId)

val fragment = supportFragmentManager.findFragmentByTag(tab.tabId) as? BrowserTabFragment
if (fragment == null) {
openNewTab(tab.tabId, tab.url, tab.skipHome, intent?.getBooleanExtra(LAUNCH_FROM_EXTERNAL_EXTRA, false) ?: false)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -232,6 +232,7 @@ import com.duckduckgo.app.pixels.AppPixelName.AUTOCOMPLETE_SEARCH_WEBSITE_SELECT
import com.duckduckgo.app.pixels.AppPixelName.ONBOARDING_DAX_CTA_CANCEL_BUTTON
import com.duckduckgo.app.pixels.AppPixelName.ONBOARDING_SEARCH_CUSTOM
import com.duckduckgo.app.pixels.AppPixelName.ONBOARDING_VISIT_SITE_CUSTOM
import com.duckduckgo.app.pixels.AppPixelName.TAB_MANAGER_CLICKED_DAILY
import com.duckduckgo.app.pixels.remoteconfig.AndroidBrowserConfigFeature
import com.duckduckgo.app.privacy.db.NetworkLeaderboardDao
import com.duckduckgo.app.privacy.db.UserAllowListRepository
Expand All @@ -246,6 +247,7 @@ import com.duckduckgo.app.statistics.pixels.Pixel.PixelValues.DAX_FIRE_DIALOG_CT
import com.duckduckgo.app.surrogates.SurrogateResponse
import com.duckduckgo.app.tabs.model.TabEntity
import com.duckduckgo.app.tabs.model.TabRepository
import com.duckduckgo.app.tabs.store.TabStatsBucketing
import com.duckduckgo.app.trackerdetection.model.TrackingEvent
import com.duckduckgo.app.usage.search.SearchCountDao
import com.duckduckgo.autofill.api.AutofillCapabilityChecker
Expand Down Expand Up @@ -344,6 +346,7 @@ import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.FlowPreview
import kotlinx.coroutines.Job
import kotlinx.coroutines.async
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
Expand Down Expand Up @@ -430,6 +433,7 @@ class BrowserTabViewModel @Inject constructor(
private val showOnAppLaunchOptionHandler: ShowOnAppLaunchOptionHandler,
private val customHeadersProvider: CustomHeadersProvider,
private val brokenSitePrompt: BrokenSitePrompt,
private val tabStatsBucketing: TabStatsBucketing,
) : WebViewClientListener,
EditSavedSiteListener,
DeleteBookmarkListener,
Expand Down Expand Up @@ -2825,7 +2829,26 @@ class BrowserTabViewModel @Inject constructor(
fun userLaunchingTabSwitcher() {
command.value = LaunchTabSwitcher
pixel.fire(AppPixelName.TAB_MANAGER_CLICKED)
pixel.fire(AppPixelName.TAB_MANAGER_CLICKED_DAILY, emptyMap(), emptyMap(), Daily())
fireDailyLaunchPixel()
}

private fun fireDailyLaunchPixel() {
val tabCount = viewModelScope.async(dispatchers.io()) { tabStatsBucketing.getNumberOfOpenTabs() }
val activeTabCount = viewModelScope.async(dispatchers.io()) { tabStatsBucketing.getTabsActiveLastWeek() }
val inactive1w = viewModelScope.async(dispatchers.io()) { tabStatsBucketing.getTabsActiveOneWeekAgo() }
val inactive2w = viewModelScope.async(dispatchers.io()) { tabStatsBucketing.getTabsActiveTwoWeeksAgo() }
val inactive3w = viewModelScope.async(dispatchers.io()) { tabStatsBucketing.getTabsActiveMoreThanThreeWeeksAgo() }

viewModelScope.launch(dispatchers.io()) {
val params = mapOf(
PixelParameter.TAB_COUNT to tabCount.await(),
PixelParameter.TAB_ACTIVE_7D to activeTabCount.await(),
PixelParameter.TAB_INACTIVE_1W to inactive1w.await(),
PixelParameter.TAB_INACTIVE_2W to inactive2w.await(),
PixelParameter.TAB_INACTIVE_3W to inactive3w.await(),
)
pixel.fire(TAB_MANAGER_CLICKED_DAILY, params, emptyMap(), Daily())
}
}

private fun isFireproofWebsite(domain: String? = site?.domain): Boolean {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -303,6 +303,12 @@ class BrowserViewModel @Inject constructor(
}
}
}

fun onTabActivated(tabId: String) {
viewModelScope.launch(dispatchers.io()) {
tabRepository.updateTabLastAccess(tabId)
}
}
}

/**
Expand Down
11 changes: 10 additions & 1 deletion app/src/main/java/com/duckduckgo/app/global/db/AppDatabase.kt
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ import com.duckduckgo.app.statistics.store.PendingPixelDao
import com.duckduckgo.app.survey.db.SurveyDao
import com.duckduckgo.app.survey.model.Survey
import com.duckduckgo.app.tabs.db.TabsDao
import com.duckduckgo.app.tabs.model.LocalDateTimeTypeConverter
import com.duckduckgo.app.tabs.model.TabEntity
import com.duckduckgo.app.tabs.model.TabSelectionEntity
import com.duckduckgo.app.trackerdetection.db.*
Expand All @@ -72,7 +73,7 @@ import com.duckduckgo.savedsites.store.SavedSitesRelationsDao

@Database(
exportSchema = true,
version = 55,
version = 56,
entities = [
TdsTracker::class,
TdsEntity::class,
Expand Down Expand Up @@ -122,6 +123,7 @@ import com.duckduckgo.savedsites.store.SavedSitesRelationsDao
LocationPermissionTypeConverter::class,
QueryParamsTypeConverter::class,
EntityTypeConverter::class,
LocalDateTimeTypeConverter::class,
)
abstract class AppDatabase : RoomDatabase() {

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

private val MIGRATION_55_TO_56: Migration = object : Migration(55, 56) {
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL("ALTER TABLE `tabs` ADD COLUMN `lastAccessTime` TEXT")
}
}

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

@Deprecated(
Expand Down
8 changes: 8 additions & 0 deletions app/src/main/java/com/duckduckgo/app/tabs/db/TabsDao.kt
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ import com.duckduckgo.app.tabs.model.TabSelectionEntity
import com.duckduckgo.common.utils.swap
import com.duckduckgo.di.scopes.AppScope
import dagger.SingleInstanceIn
import java.time.LocalDateTime
import java.time.ZoneOffset
import kotlinx.coroutines.flow.Flow

@Dao
Expand Down Expand Up @@ -165,6 +167,12 @@ abstract class TabsDao {
return tabs().lastOrNull()
}

@Query("update tabs set lastAccessTime=:lastAccessTime where tabId=:tabId")
abstract fun updateTabLastAccess(
tabId: String,
lastAccessTime: LocalDateTime = LocalDateTime.now(ZoneOffset.UTC),
)

@Query("update tabs set url=:url, title=:title, viewed=:viewed where tabId=:tabId")
abstract fun updateUrlAndTitle(
tabId: String,
Expand Down
Loading
Loading