From 72223715cf14df4a397a8d63aabe2009ccbbbf04 Mon Sep 17 00:00:00 2001 From: Devota Aabel Date: Tue, 9 Jun 2026 15:08:20 -0500 Subject: [PATCH 1/4] Bug 2043333 - Add telemetry for stories in stories screen. r=gl --- mobile/android/fenix/app/metrics.yaml | 6 ++++++ .../fenix/home/fake/FakeHomepagePreview.kt | 6 +++++- .../controller/PocketStoriesController.kt | 21 ++++++++++++++++--- .../interactor/PocketStoriesInteractor.kt | 8 ++++--- .../fenix/home/pocket/ui/PocketSection.kt | 6 +++++- .../fenix/home/pocket/ui/StoriesScreen.kt | 10 +++++++++ .../SessionControlInteractor.kt | 5 +++-- .../home/SessionControlInteractorTest.kt | 7 +++++-- .../DefaultPocketStoriesControllerTest.kt | 8 +++++-- 9 files changed, 63 insertions(+), 14 deletions(-) diff --git a/mobile/android/fenix/app/metrics.yaml b/mobile/android/fenix/app/metrics.yaml index 871ef094943db..37ce55cea36eb 100644 --- a/mobile/android/fenix/app/metrics.yaml +++ b/mobile/android/fenix/app/metrics.yaml @@ -9736,6 +9736,12 @@ pocket: type: event description: | The Pocket recommended stories are shown on the home screen. + extra_keys: + source: + type: string + description: | + Where the stories impression was recorded. + One of "homepage" or "stories_screen". bugs: - https://github.com/mozilla-mobile/fenix/issues/21593 data_reviews: diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/home/fake/FakeHomepagePreview.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/home/fake/FakeHomepagePreview.kt index 30fded6c23606..22edb1bc1a3fd 100644 --- a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/home/fake/FakeHomepagePreview.kt +++ b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/home/fake/FakeHomepagePreview.kt @@ -35,6 +35,7 @@ import org.mozilla.fenix.home.collections.CollectionsState import org.mozilla.fenix.home.interactor.HomepageInteractor import org.mozilla.fenix.home.pocket.PocketRecommendedStoriesCategory import org.mozilla.fenix.home.pocket.PocketState +import org.mozilla.fenix.home.pocket.controller.StoriesImpressionSource import org.mozilla.fenix.home.pocket.interactor.PocketStoriesInteractor import org.mozilla.fenix.home.privatebrowsing.interactor.PrivateBrowsingInteractor import org.mozilla.fenix.home.recentsyncedtabs.RecentSyncedTab @@ -140,7 +141,10 @@ internal object FakeHomepagePreview { storyPosition: Triple, ) { /* no op */ } - override fun onStoriesShown(storiesShown: List) { /* no op */ } + override fun onStoriesShown( + storiesShown: List, + source: StoriesImpressionSource, + ) { /* no op */ } override fun onCategoryClicked(categoryClicked: PocketRecommendedStoriesCategory) { /* no op */ } diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/home/pocket/controller/PocketStoriesController.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/home/pocket/controller/PocketStoriesController.kt index 2bdfedbd414de..1c3e398329c88 100644 --- a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/home/pocket/controller/PocketStoriesController.kt +++ b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/home/pocket/controller/PocketStoriesController.kt @@ -32,6 +32,15 @@ import java.lang.ref.WeakReference private const val POCKET_CATEGORIES_SELECTED_AT_A_TIME_COUNT = 8 +/** + * The surface where a Pocket stories impression was recorded. Used as the `source` extra on the + * `pocket.home_recs_shown` Glean event. + */ +enum class StoriesImpressionSource(val sourceName: String) { + HOMEPAGE("homepage"), + STORIES_SCREEN("stories_screen"), +} + /** * Contract for how all user interactions with the Pocket stories feature are to be handled. */ @@ -49,8 +58,9 @@ interface PocketStoriesController { * Callback to decide what should happen as an effect of a new list of stories being shown. * * @param storiesShown the new list of [PocketStory]es shown to the user. + * @param source the surface where the stories were shown. */ - fun handleStoriesShown(storiesShown: List) + fun handleStoriesShown(storiesShown: List, source: StoriesImpressionSource) /** * Callback allowing to handle a specific [PocketRecommendedStoriesCategory] being clicked by the user. @@ -138,7 +148,10 @@ internal class DefaultPocketStoriesController( } } - override fun handleStoriesShown(storiesShown: List) { + override fun handleStoriesShown( + storiesShown: List, + source: StoriesImpressionSource, + ) { // Only report here the impressions for recommended stories. // Sponsored stories use a different API for more accurate tracking. appStore.dispatch( @@ -149,7 +162,9 @@ internal class DefaultPocketStoriesController( ), ) - Pocket.homeRecsShown.record(NoExtras()) + Pocket.homeRecsShown.record( + Pocket.HomeRecsShownExtra(source = source.sourceName), + ) } override fun handleCategoryClick(categoryClicked: PocketRecommendedStoriesCategory) { diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/home/pocket/interactor/PocketStoriesInteractor.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/home/pocket/interactor/PocketStoriesInteractor.kt index bd0c2980b7d37..76a7189ba2508 100644 --- a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/home/pocket/interactor/PocketStoriesInteractor.kt +++ b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/home/pocket/interactor/PocketStoriesInteractor.kt @@ -8,6 +8,7 @@ import mozilla.components.service.pocket.PocketStory import mozilla.components.service.pocket.PocketStory.PocketRecommendedStory import org.mozilla.fenix.home.pocket.PocketRecommendedStoriesCategory import org.mozilla.fenix.home.pocket.controller.PocketStoriesController +import org.mozilla.fenix.home.pocket.controller.StoriesImpressionSource /** * Contract for all possible user interactions with the Pocket recommended stories feature. @@ -26,8 +27,9 @@ interface PocketStoriesInteractor { * Callback for then new stories are shown to the user. * * @param storiesShown The new list of [PocketRecommendedStory]es shown to the user. + * @param source the surface where the stories were shown. */ - fun onStoriesShown(storiesShown: List) + fun onStoriesShown(storiesShown: List, source: StoriesImpressionSource) /** * Callback for when the user clicks a specific category. @@ -66,8 +68,8 @@ class DefaultPocketStoriesInteractor( controller.handleStoryShown(storyShown, storyPosition) } - override fun onStoriesShown(storiesShown: List) { - controller.handleStoriesShown(storiesShown) + override fun onStoriesShown(storiesShown: List, source: StoriesImpressionSource) { + controller.handleStoriesShown(storiesShown, source) } override fun onCategoryClicked(categoryClicked: PocketRecommendedStoriesCategory) { diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/home/pocket/ui/PocketSection.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/home/pocket/ui/PocketSection.kt index bef29f92f2881..52f46cc582659 100644 --- a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/home/pocket/ui/PocketSection.kt +++ b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/home/pocket/ui/PocketSection.kt @@ -22,6 +22,7 @@ import org.mozilla.fenix.R import org.mozilla.fenix.compose.home.HomeSectionHeader import org.mozilla.fenix.home.fake.FakeHomepagePreview import org.mozilla.fenix.home.pocket.PocketState +import org.mozilla.fenix.home.pocket.controller.StoriesImpressionSource import org.mozilla.fenix.home.pocket.interactor.PocketStoriesInteractor import org.mozilla.fenix.theme.FirefoxTheme import org.mozilla.fenix.wallpapers.WallpaperState @@ -45,7 +46,10 @@ fun PocketSection( // We should report back when a certain story is actually being displayed. // Cannot do it reliably so for now we'll just mass report everything as being displayed. state.stories.let { - interactor.onStoriesShown(storiesShown = it) + interactor.onStoriesShown( + storiesShown = it, + source = StoriesImpressionSource.HOMEPAGE, + ) } } diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/home/pocket/ui/StoriesScreen.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/home/pocket/ui/StoriesScreen.kt index 40d44c5de12e7..06e21f3d9cbb6 100644 --- a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/home/pocket/ui/StoriesScreen.kt +++ b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/home/pocket/ui/StoriesScreen.kt @@ -34,6 +34,7 @@ import mozilla.components.compose.base.utils.BackInvokedHandler import org.mozilla.fenix.R import org.mozilla.fenix.components.appstate.recommendations.ContentRecommendationsState import org.mozilla.fenix.home.fake.FakeHomepagePreview +import org.mozilla.fenix.home.pocket.controller.StoriesImpressionSource import org.mozilla.fenix.home.pocket.interactor.PocketStoriesInteractor import org.mozilla.fenix.home.ui.LeftChevronPillButton import org.mozilla.fenix.theme.FirefoxTheme @@ -54,6 +55,15 @@ fun StoriesScreen( interactor.onDiscoverMoreScreenViewed() } + // We should report back when a certain story is actually being displayed. + // Cannot do it reliably so for now we'll just mass report everything as being displayed. + LaunchedEffect(state.pocketStories) { + interactor.onStoriesShown( + storiesShown = state.pocketStories, + source = StoriesImpressionSource.STORIES_SCREEN, + ) + } + BackInvokedHandler { onNavigationIconClick() } diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/SessionControlInteractor.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/SessionControlInteractor.kt index 6c6df71e4aca3..f3e897224703e 100644 --- a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/SessionControlInteractor.kt +++ b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/SessionControlInteractor.kt @@ -19,6 +19,7 @@ import org.mozilla.fenix.home.logo.LogoController import org.mozilla.fenix.home.logo.TrackingProtectionController import org.mozilla.fenix.home.pocket.PocketRecommendedStoriesCategory import org.mozilla.fenix.home.pocket.controller.PocketStoriesController +import org.mozilla.fenix.home.pocket.controller.StoriesImpressionSource import org.mozilla.fenix.home.privatebrowsing.controller.PrivateBrowsingController import org.mozilla.fenix.home.recentsyncedtabs.RecentSyncedTab import org.mozilla.fenix.home.recentsyncedtabs.controller.RecentSyncedTabController @@ -381,8 +382,8 @@ class SessionControlInteractor( pocketStoriesController.handleStoryShown(storyShown, storyPosition) } - override fun onStoriesShown(storiesShown: List) { - pocketStoriesController.handleStoriesShown(storiesShown) + override fun onStoriesShown(storiesShown: List, source: StoriesImpressionSource) { + pocketStoriesController.handleStoriesShown(storiesShown, source) } override fun onCategoryClicked(categoryClicked: PocketRecommendedStoriesCategory) { diff --git a/mobile/android/fenix/app/src/test/java/org/mozilla/fenix/home/SessionControlInteractorTest.kt b/mobile/android/fenix/app/src/test/java/org/mozilla/fenix/home/SessionControlInteractorTest.kt index 048e1d392ab95..1128565b356a3 100644 --- a/mobile/android/fenix/app/src/test/java/org/mozilla/fenix/home/SessionControlInteractorTest.kt +++ b/mobile/android/fenix/app/src/test/java/org/mozilla/fenix/home/SessionControlInteractorTest.kt @@ -21,6 +21,7 @@ import org.mozilla.fenix.home.logo.LogoController import org.mozilla.fenix.home.logo.TrackingProtectionController import org.mozilla.fenix.home.pocket.PocketRecommendedStoriesCategory import org.mozilla.fenix.home.pocket.controller.PocketStoriesController +import org.mozilla.fenix.home.pocket.controller.StoriesImpressionSource import org.mozilla.fenix.home.privatebrowsing.controller.PrivateBrowsingController import org.mozilla.fenix.home.recentsyncedtabs.RecentSyncedTab import org.mozilla.fenix.home.recentsyncedtabs.controller.RecentSyncedTabController @@ -245,9 +246,11 @@ class SessionControlInteractorTest { fun `GIVEN a PocketStoriesInteractor WHEN stories are shown THEN handle it in a PocketStoriesController`() { val shownStories: List = emptyList() - interactor.onStoriesShown(shownStories) + interactor.onStoriesShown(shownStories, StoriesImpressionSource.HOMEPAGE) - verify { pocketStoriesController.handleStoriesShown(shownStories) } + verify { + pocketStoriesController.handleStoriesShown(shownStories, StoriesImpressionSource.HOMEPAGE) + } } @Test diff --git a/mobile/android/fenix/app/src/test/java/org/mozilla/fenix/home/pocket/DefaultPocketStoriesControllerTest.kt b/mobile/android/fenix/app/src/test/java/org/mozilla/fenix/home/pocket/DefaultPocketStoriesControllerTest.kt index bd988aac9a15c..fcc59bb60e5b5 100644 --- a/mobile/android/fenix/app/src/test/java/org/mozilla/fenix/home/pocket/DefaultPocketStoriesControllerTest.kt +++ b/mobile/android/fenix/app/src/test/java/org/mozilla/fenix/home/pocket/DefaultPocketStoriesControllerTest.kt @@ -41,6 +41,7 @@ import org.mozilla.fenix.helpers.FenixGleanTestRule import org.mozilla.fenix.home.HomeFragmentDirections import org.mozilla.fenix.home.mars.MARSUseCases import org.mozilla.fenix.home.pocket.controller.DefaultPocketStoriesController +import org.mozilla.fenix.home.pocket.controller.StoriesImpressionSource import org.mozilla.fenix.utils.Settings import org.mozilla.fenix.utils.Stories.markAsOpenedFromHomeScreen import org.mozilla.fenix.utils.Stories.markAsOpenedFromStoriesScreen @@ -282,7 +283,7 @@ class DefaultPocketStoriesControllerTest { assertNull(Pocket.homeRecsShown.testGetValue()) - controller.handleStoriesShown(storiesShown) + controller.handleStoriesShown(storiesShown, StoriesImpressionSource.HOMEPAGE) verify { store.dispatch( @@ -297,7 +298,10 @@ class DefaultPocketStoriesControllerTest { assertNotNull(Pocket.homeRecsShown.testGetValue()) assertEquals(1, Pocket.homeRecsShown.testGetValue()!!.size) - assertNull(Pocket.homeRecsShown.testGetValue()!!.single().extra) + assertEquals( + "homepage", + Pocket.homeRecsShown.testGetValue()!!.single().extra?.get("source"), + ) } @Test From e6a40cc3176333427ae60254e3cb8ff54d814f2f Mon Sep 17 00:00:00 2001 From: Devota Aabel Date: Mon, 29 Jun 2026 14:55:57 -0500 Subject: [PATCH 2/4] Bug 2043333 - Add source extra to home_recs_story_clicked and record it on the stories screen. r=gl --- mobile/android/fenix/app/metrics.yaml | 5 + .../fenix/home/fake/FakeHomepagePreview.kt | 1 + .../controller/PocketStoriesController.kt | 9 +- .../interactor/PocketStoriesInteractor.kt | 15 ++- .../fenix/home/pocket/ui/PocketSection.kt | 4 +- .../fenix/home/pocket/ui/StoriesScreen.kt | 8 +- .../SessionControlInteractor.kt | 8 +- .../home/SessionControlInteractorTest.kt | 10 +- .../DefaultPocketStoriesControllerTest.kt | 118 ++++++++++++++++-- 9 files changed, 161 insertions(+), 17 deletions(-) diff --git a/mobile/android/fenix/app/metrics.yaml b/mobile/android/fenix/app/metrics.yaml index 37ce55cea36eb..b5f86ece59744 100644 --- a/mobile/android/fenix/app/metrics.yaml +++ b/mobile/android/fenix/app/metrics.yaml @@ -9770,6 +9770,11 @@ pocket: description: | Position of the clicked story in the list shown. Uses the [row x column] matrix notation. + source: + type: string + description: | + Where the clicked story was shown. + One of "homepage" or "stories_screen". bugs: - https://github.com/mozilla-mobile/fenix/issues/21593 data_reviews: diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/home/fake/FakeHomepagePreview.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/home/fake/FakeHomepagePreview.kt index 22edb1bc1a3fd..79cd9d55bbccb 100644 --- a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/home/fake/FakeHomepagePreview.kt +++ b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/home/fake/FakeHomepagePreview.kt @@ -151,6 +151,7 @@ internal object FakeHomepagePreview { override fun onStoryClicked( storyClicked: PocketStory, storyPosition: Triple, + source: StoriesImpressionSource, ) { /* no op */ } override fun onDiscoverMoreClicked() { /* no op */ } diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/home/pocket/controller/PocketStoriesController.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/home/pocket/controller/PocketStoriesController.kt index 1c3e398329c88..041df856528b0 100644 --- a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/home/pocket/controller/PocketStoriesController.kt +++ b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/home/pocket/controller/PocketStoriesController.kt @@ -75,8 +75,13 @@ interface PocketStoriesController { * @param storyClicked The just clicked [PocketStory]. * @param storyPosition `row x column x index` matrix representing the grid and index position * of the clicked story. + * @param source the surface where the clicked story was shown. */ - fun handleStoryClicked(storyClicked: PocketStory, storyPosition: Triple) + fun handleStoryClicked( + storyClicked: PocketStory, + storyPosition: Triple, + source: StoriesImpressionSource, + ) /** * Callback for when the user clicks on the "Discover more" button. @@ -211,6 +216,7 @@ internal class DefaultPocketStoriesController( override fun handleStoryClicked( storyClicked: PocketStory, storyPosition: Triple, + source: StoriesImpressionSource, ) { val storyUrl = when (navController.currentDestination?.id) { R.id.storiesFragment -> storyClicked.url.markAsOpenedFromStoriesScreen() @@ -231,6 +237,7 @@ internal class DefaultPocketStoriesController( Pocket.HomeRecsStoryClickedExtra( position = "${storyPosition.first}x${storyPosition.second}", timesShown = storyClicked.timesShown.inc().toString(), + source = source.sourceName, ), ) } diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/home/pocket/interactor/PocketStoriesInteractor.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/home/pocket/interactor/PocketStoriesInteractor.kt index 76a7189ba2508..4ef0b4d6457f8 100644 --- a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/home/pocket/interactor/PocketStoriesInteractor.kt +++ b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/home/pocket/interactor/PocketStoriesInteractor.kt @@ -44,8 +44,13 @@ interface PocketStoriesInteractor { * @param storyClicked The just clicked [PocketStory]. * @param storyPosition `row x column x index` matrix representing the grid and index position * of the clicked story. + * @param source the surface where the clicked story was shown. */ - fun onStoryClicked(storyClicked: PocketStory, storyPosition: Triple) + fun onStoryClicked( + storyClicked: PocketStory, + storyPosition: Triple, + source: StoriesImpressionSource, + ) /** * Callback when an user clicks on the "Discover more" nutton for stories on the homepage. @@ -76,8 +81,12 @@ class DefaultPocketStoriesInteractor( controller.handleCategoryClick(categoryClicked) } - override fun onStoryClicked(storyClicked: PocketStory, storyPosition: Triple) { - controller.handleStoryClicked(storyClicked, storyPosition) + override fun onStoryClicked( + storyClicked: PocketStory, + storyPosition: Triple, + source: StoriesImpressionSource, + ) { + controller.handleStoryClicked(storyClicked, storyPosition, source) } override fun onDiscoverMoreClicked() { diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/home/pocket/ui/PocketSection.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/home/pocket/ui/PocketSection.kt index 52f46cc582659..0d7f3880c2ea9 100644 --- a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/home/pocket/ui/PocketSection.kt +++ b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/home/pocket/ui/PocketSection.kt @@ -69,7 +69,9 @@ fun PocketSection( contentPadding = horizontalPadding, backgroundColor = cardBackgroundColor, onStoryShown = interactor::onStoryShown, - onStoryClicked = interactor::onStoryClicked, + onStoryClicked = { story, position -> + interactor.onStoryClicked(story, position, StoriesImpressionSource.HOMEPAGE) + }, ) } } diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/home/pocket/ui/StoriesScreen.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/home/pocket/ui/StoriesScreen.kt index 06e21f3d9cbb6..a14d1537550e6 100644 --- a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/home/pocket/ui/StoriesScreen.kt +++ b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/home/pocket/ui/StoriesScreen.kt @@ -165,7 +165,13 @@ private fun Stories( itemsIndexed(state.pocketStories) { index, story -> StoryCard( story = story, - onClick = interactor::onStoryClicked, + onClick = { clickedStory, position -> + interactor.onStoryClicked( + clickedStory, + position, + StoriesImpressionSource.STORIES_SCREEN, + ) + }, ) } } diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/SessionControlInteractor.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/SessionControlInteractor.kt index f3e897224703e..f9b14aeb91c1c 100644 --- a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/SessionControlInteractor.kt +++ b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/SessionControlInteractor.kt @@ -390,8 +390,12 @@ class SessionControlInteractor( pocketStoriesController.handleCategoryClick(categoryClicked) } - override fun onStoryClicked(storyClicked: PocketStory, storyPosition: Triple) { - pocketStoriesController.handleStoryClicked(storyClicked, storyPosition) + override fun onStoryClicked( + storyClicked: PocketStory, + storyPosition: Triple, + source: StoriesImpressionSource, + ) { + pocketStoriesController.handleStoryClicked(storyClicked, storyPosition, source) } override fun onDiscoverMoreClicked() { diff --git a/mobile/android/fenix/app/src/test/java/org/mozilla/fenix/home/SessionControlInteractorTest.kt b/mobile/android/fenix/app/src/test/java/org/mozilla/fenix/home/SessionControlInteractorTest.kt index 1128565b356a3..db92682347f11 100644 --- a/mobile/android/fenix/app/src/test/java/org/mozilla/fenix/home/SessionControlInteractorTest.kt +++ b/mobile/android/fenix/app/src/test/java/org/mozilla/fenix/home/SessionControlInteractorTest.kt @@ -267,9 +267,15 @@ class SessionControlInteractorTest { val clickedStory: PocketStory = mockk() val storyPosition = Triple(1, 2, 3) - interactor.onStoryClicked(clickedStory, storyPosition) + interactor.onStoryClicked(clickedStory, storyPosition, StoriesImpressionSource.HOMEPAGE) - verify { pocketStoriesController.handleStoryClicked(clickedStory, storyPosition) } + verify { + pocketStoriesController.handleStoryClicked( + clickedStory, + storyPosition, + StoriesImpressionSource.HOMEPAGE, + ) + } } @Test diff --git a/mobile/android/fenix/app/src/test/java/org/mozilla/fenix/home/pocket/DefaultPocketStoriesControllerTest.kt b/mobile/android/fenix/app/src/test/java/org/mozilla/fenix/home/pocket/DefaultPocketStoriesControllerTest.kt index fcc59bb60e5b5..899f092e923a1 100644 --- a/mobile/android/fenix/app/src/test/java/org/mozilla/fenix/home/pocket/DefaultPocketStoriesControllerTest.kt +++ b/mobile/android/fenix/app/src/test/java/org/mozilla/fenix/home/pocket/DefaultPocketStoriesControllerTest.kt @@ -304,6 +304,38 @@ class DefaultPocketStoriesControllerTest { ) } + @Test + fun `WHEN new stories are shown on the stories screen THEN update the State and record telemetry`() = runTest { + val store = spyk(AppStore()) + val controller = createController(scope = this, appStore = store) + val recommendation = mockk() + val story = mockk() + val sponsoredStory = mockk() + val storiesShown = listOf(recommendation, story, sponsoredStory) + + assertNull(Pocket.homeRecsShown.testGetValue()) + + controller.handleStoriesShown(storiesShown, StoriesImpressionSource.STORIES_SCREEN) + + verify { + store.dispatch( + ContentRecommendationsAction.PocketStoriesShown( + impressions = listOf( + PocketImpression(story = recommendation, position = 0), + PocketImpression(story = story, position = 1), + ), + ), + ) + } + + assertNotNull(Pocket.homeRecsShown.testGetValue()) + assertEquals(1, Pocket.homeRecsShown.testGetValue()!!.size) + assertEquals( + "stories_screen", + Pocket.homeRecsShown.testGetValue()!!.single().extra?.get("source"), + ) + } + @Test fun `WHEN a recommended story is clicked THEN open that story's url using HomeActivity and record telemetry`() = runTest { val story = PocketRecommendedStory( @@ -318,7 +350,11 @@ class DefaultPocketStoriesControllerTest { val controller = createController(scope = this) assertNull(Pocket.homeRecsStoryClicked.testGetValue()) - controller.handleStoryClicked(story, storyPosition = Triple(1, 2, 3)) + controller.handleStoryClicked( + story, + storyPosition = Triple(1, 2, 3), + source = StoriesImpressionSource.HOMEPAGE, + ) verify { navController.navigate(R.id.browserFragment) @@ -336,6 +372,48 @@ class DefaultPocketStoriesControllerTest { assertEquals("1x2", event.single().extra!!["position"]) assertTrue(event.single().extra!!.containsKey("times_shown")) assertEquals(story.timesShown.inc().toString(), event.single().extra!!["times_shown"]) + assertTrue(event.single().extra!!.containsKey("source")) + assertEquals("homepage", event.single().extra!!["source"]) + } + + @Test + fun `WHEN a recommended story is clicked on the stories screen THEN open that story's url using HomeActivity and record telemetry`() = runTest { + val story = PocketRecommendedStory( + title = "", + url = "testLink", + imageUrl = "", + publisher = "", + category = "", + timeToRead = 0, + timesShown = 123, + ) + val controller = createController(scope = this) + assertNull(Pocket.homeRecsStoryClicked.testGetValue()) + + controller.handleStoryClicked( + story, + storyPosition = Triple(1, 2, 3), + source = StoriesImpressionSource.STORIES_SCREEN, + ) + + verify { + navController.navigate(R.id.browserFragment) + fenixBrowserUseCases.loadUrlOrSearch( + searchTermOrURL = story.url, + newTab = true, + private = false, + ) + } + + assertNotNull(Pocket.homeRecsStoryClicked.testGetValue()) + val event = Pocket.homeRecsStoryClicked.testGetValue()!! + assertEquals(1, event.size) + assertTrue(event.single().extra!!.containsKey("position")) + assertEquals("1x2", event.single().extra!!["position"]) + assertTrue(event.single().extra!!.containsKey("times_shown")) + assertEquals(story.timesShown.inc().toString(), event.single().extra!!["times_shown"]) + assertTrue(event.single().extra!!.containsKey("source")) + assertEquals("stories_screen", event.single().extra!!["source"]) } @Test @@ -354,7 +432,11 @@ class DefaultPocketStoriesControllerTest { val controller = createController(scope = this) assertNull(Pocket.homeRecsStoryClicked.testGetValue()) - controller.handleStoryClicked(story, storyPosition = Triple(1, 2, 3)) + controller.handleStoryClicked( + story, + storyPosition = Triple(1, 2, 3), + source = StoriesImpressionSource.HOMEPAGE, + ) verify { navController.navigate(R.id.browserFragment) @@ -372,6 +454,8 @@ class DefaultPocketStoriesControllerTest { assertEquals("1x2", event.single().extra!!["position"]) assertTrue(event.single().extra!!.containsKey("times_shown")) assertEquals(story.timesShown.inc().toString(), event.single().extra!!["times_shown"]) + assertTrue(event.single().extra!!.containsKey("source")) + assertEquals("homepage", event.single().extra!!["source"]) } @Test @@ -395,7 +479,11 @@ class DefaultPocketStoriesControllerTest { assertNull(Pocket.homeRecsSpocClicked.testGetValue()) - controller.handleStoryClicked(sponsoredContent, storyPosition = Triple(2, 3, 4)) + controller.handleStoryClicked( + sponsoredContent, + storyPosition = Triple(2, 3, 4), + source = StoriesImpressionSource.HOMEPAGE, + ) testScheduler.advanceUntilIdle() assertEquals(1, Pocket.homeRecsSpocClicked.testGetValue()!!.size) @@ -420,7 +508,11 @@ class DefaultPocketStoriesControllerTest { val story = PocketRecommendedStory("", "url", "", "", "", 0, 0) val controller = createController(scope = this) - controller.handleStoryClicked(story, storyPosition = Triple(1, 2, 3)) + controller.handleStoryClicked( + story, + storyPosition = Triple(1, 2, 3), + source = StoriesImpressionSource.HOMEPAGE, + ) verifyOrder { navController.navigate(R.id.browserFragment) @@ -439,7 +531,11 @@ class DefaultPocketStoriesControllerTest { val story = PocketRecommendedStory("", "url", "", "", "", 0, 0) val controller = createController(scope = this) - controller.handleStoryClicked(story, storyPosition = Triple(1, 2, 3)) + controller.handleStoryClicked( + story, + storyPosition = Triple(1, 2, 3), + source = StoriesImpressionSource.HOMEPAGE, + ) verifyOrder { navController.navigate(R.id.browserFragment) @@ -458,7 +554,11 @@ class DefaultPocketStoriesControllerTest { val story = PocketRecommendedStory("", originalURL, "", "", "", 0, 0) val controller = createController(scope = this) - controller.handleStoryClicked(story, storyPosition = Triple(1, 2, 3)) + controller.handleStoryClicked( + story, + storyPosition = Triple(1, 2, 3), + source = StoriesImpressionSource.HOMEPAGE, + ) verifyOrder { navController.navigate(R.id.browserFragment) @@ -477,7 +577,11 @@ class DefaultPocketStoriesControllerTest { val story = PocketRecommendedStory("", originalURL, "", "", "", 0, 0) val controller = createController(scope = this) - controller.handleStoryClicked(story, storyPosition = Triple(1, 2, 3)) + controller.handleStoryClicked( + story, + storyPosition = Triple(1, 2, 3), + source = StoriesImpressionSource.HOMEPAGE, + ) verifyOrder { navController.navigate(R.id.browserFragment) From f12911b973d7868711c0e2285e2e79d6a7887429 Mon Sep 17 00:00:00 2001 From: Devota Aabel Date: Mon, 29 Jun 2026 17:53:49 -0500 Subject: [PATCH 3/4] Bug 2043333 - Add source extra to home.content.article impression and click events. r=gl --- mobile/android/fenix/app/metrics.yaml | 6 ++ .../fenix/components/appstate/AppAction.kt | 10 ++- .../middleware/HomeTelemetryMiddleware.kt | 2 + .../controller/PocketStoriesController.kt | 3 + .../mozilla/fenix/components/AppStoreTest.kt | 3 + .../fenix/home/PocketMiddlewareTest.kt | 2 + .../middleware/HomeTelemetryMiddlewareTest.kt | 73 +++++++++++++++++++ .../DefaultPocketStoriesControllerTest.kt | 4 + 8 files changed, 101 insertions(+), 2 deletions(-) diff --git a/mobile/android/fenix/app/metrics.yaml b/mobile/android/fenix/app/metrics.yaml index b5f86ece59744..5eae07bdb0770 100644 --- a/mobile/android/fenix/app/metrics.yaml +++ b/mobile/android/fenix/app/metrics.yaml @@ -15368,6 +15368,11 @@ home.content.article: topic: &topic description: The topic of the recommendation. Like "entertainment". type: string + source: &source_pocket + description: > + The surface where the article was shown. + One of "homepage" or "stories_screen". + type: string send_in_pings: - home @@ -15393,6 +15398,7 @@ home.content.article: received_rank: *received_rank recommended_at: *recommended_at topic: *topic + source: *source_pocket send_in_pings: - home diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/components/appstate/AppAction.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/components/appstate/AppAction.kt index fcb93f704eafa..850df8c52ff1f 100644 --- a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/components/appstate/AppAction.kt +++ b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/components/appstate/AppAction.kt @@ -31,6 +31,7 @@ import org.mozilla.fenix.home.bookmarks.Bookmark import org.mozilla.fenix.home.pocket.PocketImpression import org.mozilla.fenix.home.pocket.PocketRecommendedStoriesCategory import org.mozilla.fenix.home.pocket.PocketRecommendedStoriesSelectedCategory +import org.mozilla.fenix.home.pocket.controller.StoriesImpressionSource import org.mozilla.fenix.home.recentsyncedtabs.RecentSyncedTab import org.mozilla.fenix.home.recentsyncedtabs.RecentSyncedTabState import org.mozilla.fenix.home.recenttabs.RecentTab @@ -598,10 +599,12 @@ sealed class AppAction : Action { * * @property recommendation The [ContentRecommendation] that was clicked. * @property position The position (0-index) of the [ContentRecommendation]. + * @property source The surface where the clicked recommendation was shown. */ data class ContentRecommendationClicked( val recommendation: ContentRecommendation, val position: Int, + val source: StoriesImpressionSource, ) : ContentRecommendationsAction() /** @@ -621,9 +624,12 @@ sealed class AppAction : Action { * * @property impressions A list of [PocketImpression]s detailing the story shown and * their respective position. + * @property source The surface where the stories were shown. */ - data class PocketStoriesShown(val impressions: List) : - ContentRecommendationsAction() + data class PocketStoriesShown( + val impressions: List, + val source: StoriesImpressionSource, + ) : ContentRecommendationsAction() /** * Cleans all in-memory data about Pocket stories and categories. diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/home/middleware/HomeTelemetryMiddleware.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/home/middleware/HomeTelemetryMiddleware.kt index 8d6d29fcf8ccc..fe4c7aea913a9 100644 --- a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/home/middleware/HomeTelemetryMiddleware.kt +++ b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/home/middleware/HomeTelemetryMiddleware.kt @@ -40,6 +40,7 @@ class HomeTelemetryMiddleware : Middleware { scheduledCorpusItemId = recommendation.scheduledCorpusItemId, tileId = recommendation.tileId.toInt(), topic = recommendation.topic, + source = action.source.sourceName, ), ) @@ -60,6 +61,7 @@ class HomeTelemetryMiddleware : Middleware { scheduledCorpusItemId = story.scheduledCorpusItemId, tileId = story.tileId.toInt(), topic = story.topic, + source = action.source.sourceName, ), ) } diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/home/pocket/controller/PocketStoriesController.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/home/pocket/controller/PocketStoriesController.kt index 041df856528b0..0e98977afe53a 100644 --- a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/home/pocket/controller/PocketStoriesController.kt +++ b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/home/pocket/controller/PocketStoriesController.kt @@ -129,6 +129,7 @@ internal class DefaultPocketStoriesController( position = storyPosition.third, ), ), + source = StoriesImpressionSource.HOMEPAGE, ), ) @@ -164,6 +165,7 @@ internal class DefaultPocketStoriesController( impressions = storiesShown .filter { it is ContentRecommendation || it is PocketRecommendedStory } .map { PocketImpression(story = it, position = storiesShown.indexOf(it)) }, + source = source, ), ) @@ -247,6 +249,7 @@ internal class DefaultPocketStoriesController( ContentRecommendationsAction.ContentRecommendationClicked( recommendation = storyClicked, position = storyPosition.third, + source = source, ), ) } diff --git a/mobile/android/fenix/app/src/test/java/org/mozilla/fenix/components/AppStoreTest.kt b/mobile/android/fenix/app/src/test/java/org/mozilla/fenix/components/AppStoreTest.kt index 82e4091cb383f..d084356ad4adf 100644 --- a/mobile/android/fenix/app/src/test/java/org/mozilla/fenix/components/AppStoreTest.kt +++ b/mobile/android/fenix/app/src/test/java/org/mozilla/fenix/components/AppStoreTest.kt @@ -44,6 +44,7 @@ import org.mozilla.fenix.home.pocket.POCKET_STORIES_DEFAULT_CATEGORY_NAME import org.mozilla.fenix.home.pocket.PocketImpression import org.mozilla.fenix.home.pocket.PocketRecommendedStoriesCategory import org.mozilla.fenix.home.pocket.PocketRecommendedStoriesSelectedCategory +import org.mozilla.fenix.home.pocket.controller.StoriesImpressionSource import org.mozilla.fenix.home.recentsyncedtabs.RecentSyncedTab import org.mozilla.fenix.home.recentsyncedtabs.RecentSyncedTabState import org.mozilla.fenix.home.recenttabs.RecentTab @@ -565,6 +566,7 @@ class AppStoreTest { PocketImpression(story = sponsoredContent, position = 0), PocketImpression(story = sponsoredContent3, position = 2), ), + source = StoriesImpressionSource.HOMEPAGE, ), ) @@ -615,6 +617,7 @@ class AppStoreTest { PocketImpression(story = recommendation1, position = 0), PocketImpression(story = recommendation3, position = 2), ), + source = StoriesImpressionSource.HOMEPAGE, ), ) diff --git a/mobile/android/fenix/app/src/test/java/org/mozilla/fenix/home/PocketMiddlewareTest.kt b/mobile/android/fenix/app/src/test/java/org/mozilla/fenix/home/PocketMiddlewareTest.kt index 7e6c7c4d62448..1fe917d0c206d 100644 --- a/mobile/android/fenix/app/src/test/java/org/mozilla/fenix/home/PocketMiddlewareTest.kt +++ b/mobile/android/fenix/app/src/test/java/org/mozilla/fenix/home/PocketMiddlewareTest.kt @@ -33,6 +33,7 @@ import org.mozilla.fenix.datastore.SelectedPocketStoriesCategories.SelectedPocke import org.mozilla.fenix.home.pocket.PocketImpression import org.mozilla.fenix.home.pocket.PocketRecommendedStoriesCategory import org.mozilla.fenix.home.pocket.PocketRecommendedStoriesSelectedCategory +import org.mozilla.fenix.home.pocket.controller.StoriesImpressionSource class PocketMiddlewareTest { @@ -74,6 +75,7 @@ class PocketMiddlewareTest { position = 1, ), ), + source = StoriesImpressionSource.HOMEPAGE, ), ) testScheduler.advanceUntilIdle() diff --git a/mobile/android/fenix/app/src/test/java/org/mozilla/fenix/home/middleware/HomeTelemetryMiddlewareTest.kt b/mobile/android/fenix/app/src/test/java/org/mozilla/fenix/home/middleware/HomeTelemetryMiddlewareTest.kt index 1067874be2644..5f7bff53811aa 100644 --- a/mobile/android/fenix/app/src/test/java/org/mozilla/fenix/home/middleware/HomeTelemetryMiddlewareTest.kt +++ b/mobile/android/fenix/app/src/test/java/org/mozilla/fenix/home/middleware/HomeTelemetryMiddlewareTest.kt @@ -20,6 +20,7 @@ import org.mozilla.fenix.components.AppStore import org.mozilla.fenix.components.appstate.AppAction.ContentRecommendationsAction import org.mozilla.fenix.helpers.FenixGleanTestRule import org.mozilla.fenix.home.pocket.PocketImpression +import org.mozilla.fenix.home.pocket.controller.StoriesImpressionSource import kotlin.test.assertNotNull @RunWith(AndroidJUnit4::class) @@ -55,6 +56,7 @@ class HomeTelemetryMiddlewareTest { assertEquals(recommendation.topic, extraValues["topic"]) assertEquals(position.toString(), extraValues["position"]) assertEquals("false", extraValues["is_sponsored"]) + assertEquals("homepage", extraValues["source"]) pingReceived = true } @@ -63,6 +65,38 @@ class HomeTelemetryMiddlewareTest { ContentRecommendationsAction.ContentRecommendationClicked( recommendation = recommendation, position = position, + source = StoriesImpressionSource.HOMEPAGE, + ), + ) + + job.join() + assertTrue(pingReceived) + } + + @Test + fun `WHEN a recommendation is clicked on the stories screen THEN record the click telemetry with the stories screen source`() { + val store = createStore() + val recommendation = TestUtils.getFakeContentRecommendations(limit = 1).first() + val position = 100 + + assertNull(HomeContentArticle.click.testGetValue()) + + var pingReceived = false + val job = Pings.home.testBeforeNextSubmit { + assertNotNull(HomeContentArticle.click.testGetValue()) + + val snapshot = HomeContentArticle.click.testGetValue()!! + assertEquals(1, snapshot.size) + assertEquals("stories_screen", snapshot.first().extra!!["source"]) + + pingReceived = true + } + + store.dispatch( + ContentRecommendationsAction.ContentRecommendationClicked( + recommendation = recommendation, + position = position, + source = StoriesImpressionSource.STORIES_SCREEN, ), ) @@ -104,6 +138,44 @@ class HomeTelemetryMiddlewareTest { assertEquals(recommendation.topic, extraValues["topic"]) assertEquals(position.toString(), extraValues["position"]) assertEquals("false", extraValues["is_sponsored"]) + assertEquals("homepage", extraValues["source"]) + } + + pingReceived = true + } + + store.dispatch( + ContentRecommendationsAction.PocketStoriesShown( + impressions = impressions, + source = StoriesImpressionSource.HOMEPAGE, + ), + ) + + job.join() + assertTrue(pingReceived) + } + + @Test + fun `WHEN a list of recommendations are shown on the stories screen THEN record the impression telemetry with the stories screen source`() { + val store = createStore() + val impressions = TestUtils.getFakeContentRecommendations(limit = 3) + .mapIndexed { index, contentRecommendation -> + PocketImpression( + story = contentRecommendation, + position = index, + ) + } + + assertNull(HomeContentArticle.impression.testGetValue()) + + var pingReceived = false + val job = Pings.home.testBeforeNextSubmit { + assertNotNull(HomeContentArticle.impression.testGetValue()) + + val snapshot = HomeContentArticle.impression.testGetValue()!! + assertEquals(3, snapshot.size) + snapshot.forEach { + assertEquals("stories_screen", it.extra!!["source"]) } pingReceived = true @@ -112,6 +184,7 @@ class HomeTelemetryMiddlewareTest { store.dispatch( ContentRecommendationsAction.PocketStoriesShown( impressions = impressions, + source = StoriesImpressionSource.STORIES_SCREEN, ), ) diff --git a/mobile/android/fenix/app/src/test/java/org/mozilla/fenix/home/pocket/DefaultPocketStoriesControllerTest.kt b/mobile/android/fenix/app/src/test/java/org/mozilla/fenix/home/pocket/DefaultPocketStoriesControllerTest.kt index 899f092e923a1..2ab49153726d5 100644 --- a/mobile/android/fenix/app/src/test/java/org/mozilla/fenix/home/pocket/DefaultPocketStoriesControllerTest.kt +++ b/mobile/android/fenix/app/src/test/java/org/mozilla/fenix/home/pocket/DefaultPocketStoriesControllerTest.kt @@ -227,6 +227,7 @@ class DefaultPocketStoriesControllerTest { impressions = listOf( PocketImpression(story = storyShown, position = 3), ), + source = StoriesImpressionSource.HOMEPAGE, ), ) } @@ -267,6 +268,7 @@ class DefaultPocketStoriesControllerTest { impressions = listOf( PocketImpression(story = sponsoredContent, position = 3), ), + source = StoriesImpressionSource.HOMEPAGE, ), ) } @@ -292,6 +294,7 @@ class DefaultPocketStoriesControllerTest { PocketImpression(story = recommendation, position = 0), PocketImpression(story = story, position = 1), ), + source = StoriesImpressionSource.HOMEPAGE, ), ) } @@ -324,6 +327,7 @@ class DefaultPocketStoriesControllerTest { PocketImpression(story = recommendation, position = 0), PocketImpression(story = story, position = 1), ), + source = StoriesImpressionSource.STORIES_SCREEN, ), ) } From e15a8e55380f5dd1772f5f2dddb682247b5d80b6 Mon Sep 17 00:00:00 2001 From: Devota Aabel Date: Tue, 30 Jun 2026 16:17:33 -0500 Subject: [PATCH 4/4] Bug 1996675 - Add homepage/shortcuts_library source to top sites telemetry. r=gl --- mobile/android/fenix/app/metrics.yaml | 37 ++- .../org/mozilla/fenix/home/HomeFragment.kt | 2 + .../fenix/home/topsites/ShortcutsFragment.kt | 2 + .../topsites/controller/TopSiteController.kt | 64 ++++-- .../DefaultTopSiteControllerTest.kt | 213 ++++++++++++++---- 5 files changed, 253 insertions(+), 65 deletions(-) diff --git a/mobile/android/fenix/app/metrics.yaml b/mobile/android/fenix/app/metrics.yaml index 5eae07bdb0770..c8323ade0faf4 100644 --- a/mobile/android/fenix/app/metrics.yaml +++ b/mobile/android/fenix/app/metrics.yaml @@ -9221,6 +9221,12 @@ top_sites: type: event description: | A user opened a default top site + extra_keys: + source: &top_sites_source + type: string + description: | + The surface where the top site interaction occurred. + One of "homepage" or "shortcuts_library". bugs: - https://github.com/mozilla-mobile/fenix/issues/8125 data_reviews: @@ -9241,6 +9247,8 @@ top_sites: type: event description: | A user opened the google top site + extra_keys: + source: *top_sites_source bugs: - https://github.com/mozilla-mobile/fenix/issues/17418 data_reviews: @@ -9281,6 +9289,8 @@ top_sites: type: event description: | A user opened a frecency top site + extra_keys: + source: *top_sites_source bugs: - https://github.com/mozilla-mobile/fenix/issues/14565 data_reviews: @@ -9301,6 +9311,8 @@ top_sites: type: event description: | A user opened a pinned top site + extra_keys: + source: *top_sites_source bugs: - https://github.com/mozilla-mobile/fenix/issues/14565 data_reviews: @@ -9352,6 +9364,7 @@ top_sites: description: | The type of top site. Options are: "FRECENCY", "DEFAULT", "PINNED", or "PROVIDED" + source: *top_sites_source bugs: - https://github.com/mozilla-mobile/fenix/issues/14565 - https://github.com/mozilla-mobile/fenix/issues/23526 @@ -9374,6 +9387,8 @@ top_sites: type: event description: | A user opens a new tab based on a top site item + extra_keys: + source: *top_sites_source bugs: - https://github.com/mozilla-mobile/fenix/issues/6757 data_reviews: @@ -9394,6 +9409,8 @@ top_sites: type: event description: | A user opens a new private tab based on a top site item + extra_keys: + source: *top_sites_source bugs: - https://github.com/mozilla-mobile/fenix/issues/6757 data_reviews: @@ -9414,6 +9431,8 @@ top_sites: type: event description: | A user removes a top site item + extra_keys: + source: *top_sites_source bugs: - https://github.com/mozilla-mobile/fenix/issues/6757 data_reviews: @@ -9434,6 +9453,8 @@ top_sites: type: event description: | A user removed the default Google top site + extra_keys: + source: *top_sites_source bugs: - https://github.com/mozilla-mobile/fenix/issues/21841 data_reviews: @@ -9466,6 +9487,8 @@ top_sites: type: event description: | A user has opened a contile top site. + extra_keys: + source: *top_sites_source bugs: - https://github.com/mozilla-mobile/fenix/issues/23526 data_reviews: @@ -9482,6 +9505,8 @@ top_sites: description: | A user has opened a contile top site in a private tab via the long press context menu. + extra_keys: + source: *top_sites_source bugs: - https://github.com/mozilla-mobile/fenix/issues/23526 data_reviews: @@ -9498,6 +9523,8 @@ top_sites: description: | A user has selected "Settings" via the contile top sites longpress context menu. + extra_keys: + source: *top_sites_source bugs: - https://github.com/mozilla-mobile/fenix/issues/23526 data_reviews: @@ -9514,6 +9541,8 @@ top_sites: description: | A user has selected "Our sponsors & your privacy" via the contile top sites longpress context menu. + extra_keys: + source: *top_sites_source bugs: - https://github.com/mozilla-mobile/fenix/issues/23526 data_reviews: @@ -9618,7 +9647,9 @@ top_sites: A user saw a Contile top site extra_keys: source: - description: The source of the event, example "newtab", "urlbar" + description: | + The surface where the Contile top site was shown. + One of "homepage" or "shortcuts_library". type: string position: description: The tile placement (1-based) @@ -9645,7 +9676,9 @@ top_sites: A user clicked a Contile top site extra_keys: source: - description: The source of the event, example "newtab", "urlbar" + description: | + The surface where the Contile top site was clicked. + One of "homepage" or "shortcuts_library". type: string position: description: The tile placement (1-based) diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/home/HomeFragment.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/home/HomeFragment.kt index e27c8155baa5e..b2abe6b84fcca 100644 --- a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/home/HomeFragment.kt +++ b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/home/HomeFragment.kt @@ -141,6 +141,7 @@ import org.mozilla.fenix.home.toolbar.HomeToolbarComposable.Companion.DirectToSe import org.mozilla.fenix.home.topsites.DefaultTopSitesView import org.mozilla.fenix.home.topsites.TopSitesBinding import org.mozilla.fenix.home.topsites.controller.DefaultTopSiteController +import org.mozilla.fenix.home.topsites.controller.TopSitesSource import org.mozilla.fenix.home.topsites.getTopSitesConfig import org.mozilla.fenix.home.ui.Homepage import org.mozilla.fenix.home.ui.WallpaperBackground @@ -1339,6 +1340,7 @@ class HomeFragment : Fragment() { marsUseCases = requireComponents.useCases.marsUseCases, mozAdsUseCases = requireComponents.useCases.mozAdsUseCases, viewLifecycleScope = viewLifecycleOwner.lifecycleScope, + source = TopSitesSource.HOMEPAGE, ) private fun initController() { diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/home/topsites/ShortcutsFragment.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/home/topsites/ShortcutsFragment.kt index 7a6f643713179..b5f73920f54b0 100644 --- a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/home/topsites/ShortcutsFragment.kt +++ b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/home/topsites/ShortcutsFragment.kt @@ -19,6 +19,7 @@ import org.mozilla.fenix.e2e.SystemInsetsPaddedFragment import org.mozilla.fenix.ext.requireComponents import org.mozilla.fenix.home.topsites.controller.DefaultTopSiteController import org.mozilla.fenix.home.topsites.controller.TopSiteController +import org.mozilla.fenix.home.topsites.controller.TopSitesSource import org.mozilla.fenix.home.topsites.interactor.DefaultTopSiteInteractor import org.mozilla.fenix.home.topsites.interactor.TopSiteInteractor import org.mozilla.fenix.home.topsites.middleware.ShortcutsMiddleware @@ -70,6 +71,7 @@ class ShortcutsFragment : Fragment(), SystemInsetsPaddedFragment { marsUseCases = requireComponents.useCases.marsUseCases, mozAdsUseCases = requireComponents.useCases.mozAdsUseCases, viewLifecycleScope = viewLifecycleOwner.lifecycleScope, + source = TopSitesSource.SHORTCUTS_LIBRARY, ) interactor = DefaultTopSiteInteractor( diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/home/topsites/controller/TopSiteController.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/home/topsites/controller/TopSiteController.kt index 5923a25767b7a..a7857e48e7978 100644 --- a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/home/topsites/controller/TopSiteController.kt +++ b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/home/topsites/controller/TopSiteController.kt @@ -53,6 +53,15 @@ import java.lang.ref.WeakReference import androidx.appcompat.R as appcompatR import mozilla.components.ui.icons.R as iconsR +/** + * The surface where a top sites / shortcuts interaction occurred. Used as the `source` extra on + * the `top_sites` Glean events. + */ +enum class TopSitesSource(val sourceName: String) { + HOMEPAGE("homepage"), + SHORTCUTS_LIBRARY("shortcuts_library"), +} + /** * An interface that handles the view manipulation of the top sites triggered by the Interactor. */ @@ -129,6 +138,7 @@ class DefaultTopSiteController( private val marsUseCases: MARSUseCases, private val mozAdsUseCases: MozAdsUseCases, private val viewLifecycleScope: CoroutineScope, + private val source: TopSitesSource, ) : TopSiteController { private val activity: Activity @@ -139,9 +149,13 @@ class DefaultTopSiteController( override fun handleOpenInPrivateTabClicked(topSite: TopSite) { if (topSite is TopSite.Provided) { - TopSites.openContileInPrivateTab.record(NoExtras()) + TopSites.openContileInPrivateTab.record( + TopSites.OpenContileInPrivateTabExtra(source = source.sourceName), + ) } else { - TopSites.openInPrivateTab.record(NoExtras()) + TopSites.openInPrivateTab.record( + TopSites.OpenInPrivateTabExtra(source = source.sourceName), + ) } activity.components.appStore.dispatch( @@ -237,10 +251,12 @@ class DefaultTopSiteController( } override fun handleRemoveTopSiteClicked(topSite: TopSite) { - TopSites.remove.record(NoExtras()) + TopSites.remove.record(TopSites.RemoveExtra(source = source.sourceName)) when (topSite.url) { - SupportUtils.GOOGLE_URL -> TopSites.googleTopSiteRemoved.record(NoExtras()) + SupportUtils.GOOGLE_URL -> TopSites.googleTopSiteRemoved.record( + TopSites.GoogleTopSiteRemovedExtra(source = source.sourceName), + ) } viewLifecycleScope.launch { @@ -252,9 +268,15 @@ class DefaultTopSiteController( override fun handleSelectTopSite(topSite: TopSite, position: Int) { when (topSite) { - is TopSite.Default -> TopSites.openDefault.record(NoExtras()) - is TopSite.Frecent -> TopSites.openFrecency.record(NoExtras()) - is TopSite.Pinned -> TopSites.openPinned.record(NoExtras()) + is TopSite.Default -> TopSites.openDefault.record( + TopSites.OpenDefaultExtra(source = source.sourceName), + ) + is TopSite.Frecent -> TopSites.openFrecency.record( + TopSites.OpenFrecencyExtra(source = source.sourceName), + ) + is TopSite.Pinned -> TopSites.openPinned.record( + TopSites.OpenPinnedExtra(source = source.sourceName), + ) is TopSite.Provided -> { if (settings.enableMozillaAdsClient) { sendMozAdsClickInteraction(clickUrl = topSite.clickUrl) @@ -262,14 +284,18 @@ class DefaultTopSiteController( sendMarsTopSiteCallback(topSite.clickUrl) } - TopSites.openContileTopSite.record(NoExtras()).also { + TopSites.openContileTopSite.record( + TopSites.OpenContileTopSiteExtra(source = source.sourceName), + ).also { recordTopSitesClickTelemetry(topSite, position) } } } when (topSite.url) { - SupportUtils.GOOGLE_URL -> TopSites.openGoogleSearchAttribution.record(NoExtras()) + SupportUtils.GOOGLE_URL -> TopSites.openGoogleSearchAttribution.record( + TopSites.OpenGoogleSearchAttributionExtra(source = source.sourceName), + ) } val availableEngines: List = getAvailableSearchEngines() @@ -302,7 +328,9 @@ class DefaultTopSiteController( } if (existingTabForUrl == null) { - TopSites.openInNewTab.record(NoExtras()) + TopSites.openInNewTab.record( + TopSites.OpenInNewTabExtra(source = source.sourceName), + ) addTabUseCase.invoke( url = appendSearchAttributionToUrlIfNeeded(topSite.url), @@ -326,7 +354,7 @@ class DefaultTopSiteController( TopSites.contileClick.record( TopSites.ContileClickExtra( position = position + 1, - source = "newtab", + source = source.sourceName, ), ) @@ -346,7 +374,7 @@ class DefaultTopSiteController( TopSites.contileImpression.record( TopSites.ContileImpressionExtra( position = position + 1, - source = "newtab", + source = source.sourceName, ), ) @@ -375,12 +403,16 @@ class DefaultTopSiteController( } override fun handleTopSiteSettingsClicked() { - TopSites.contileSettings.record(NoExtras()) + TopSites.contileSettings.record( + TopSites.ContileSettingsExtra(source = source.sourceName), + ) navController.navigate(R.id.homeSettingsFragment) } override fun handleSponsorPrivacyClicked() { - TopSites.contileSponsorsAndPrivacy.record(NoExtras()) + TopSites.contileSponsorsAndPrivacy.record( + TopSites.ContileSponsorsAndPrivacyExtra(source = source.sourceName), + ) if (navController.currentDestination?.id == R.id.shortcutsFragment) { navController.navigate(ShortcutsFragmentDirections.actionShortcutsFragmentToBrowserFragment()) @@ -396,7 +428,9 @@ class DefaultTopSiteController( } override fun handleTopSiteLongClicked(topSite: TopSite) { - TopSites.longPress.record(TopSites.LongPressExtra(topSite.type)) + TopSites.longPress.record( + TopSites.LongPressExtra(type = topSite.type, source = source.sourceName), + ) } override fun handleShowAllTopSitesClicked() { diff --git a/mobile/android/fenix/app/src/test/java/org/mozilla/fenix/home/topsites/controller/DefaultTopSiteControllerTest.kt b/mobile/android/fenix/app/src/test/java/org/mozilla/fenix/home/topsites/controller/DefaultTopSiteControllerTest.kt index ea6650f91d733..99fe3fd552c5c 100644 --- a/mobile/android/fenix/app/src/test/java/org/mozilla/fenix/home/topsites/controller/DefaultTopSiteControllerTest.kt +++ b/mobile/android/fenix/app/src/test/java/org/mozilla/fenix/home/topsites/controller/DefaultTopSiteControllerTest.kt @@ -128,11 +128,11 @@ class DefaultTopSiteControllerTest { assertNotNull(TopSites.openInNewTab.testGetValue()) assertEquals(1, TopSites.openInNewTab.testGetValue()!!.size) - assertNull(TopSites.openInNewTab.testGetValue()!!.single().extra) + assertEquals("homepage", TopSites.openInNewTab.testGetValue()!!.single().extra!!["source"]) assertNotNull(TopSites.openDefault.testGetValue()) assertEquals(1, TopSites.openDefault.testGetValue()!!.size) - assertNull(TopSites.openDefault.testGetValue()!!.single().extra) + assertEquals("homepage", TopSites.openDefault.testGetValue()!!.single().extra!!["source"]) verify { tabsUseCases.addTab.invoke( @@ -160,7 +160,7 @@ class DefaultTopSiteControllerTest { assertNotNull(TopSites.openInNewTab.testGetValue()) assertEquals(1, TopSites.openInNewTab.testGetValue()!!.size) - assertNull(TopSites.openInNewTab.testGetValue()!!.single().extra) + assertEquals("homepage", TopSites.openInNewTab.testGetValue()!!.single().extra!!["source"]) verify { tabsUseCases.addTab.invoke( @@ -209,7 +209,7 @@ class DefaultTopSiteControllerTest { url = "mozilla.org", createdAt = 0, ) - val controller = spyk(createController(this)) + val controller = spyk(createController(this, source = TopSitesSource.SHORTCUTS_LIBRARY)) every { controller.getAvailableSearchEngines() } returns listOf(searchEngine) @@ -225,6 +225,33 @@ class DefaultTopSiteControllerTest { } } + @Test + fun `GIVEN the shortcuts library source WHEN a top site is selected THEN record telemetry with the shortcuts library source`() = runTest { + val topSite = TopSite.Default( + id = 1L, + title = "Mozilla", + url = "mozilla.org", + createdAt = 0, + ) + val controller = spyk(createController(this, source = TopSitesSource.SHORTCUTS_LIBRARY)) + + every { controller.getAvailableSearchEngines() } returns listOf(searchEngine) + + controller.handleSelectTopSite(topSite, position = 0) + + assertNotNull(TopSites.openInNewTab.testGetValue()) + assertEquals( + "shortcuts_library", + TopSites.openInNewTab.testGetValue()!!.single().extra!!["source"], + ) + + assertNotNull(TopSites.openDefault.testGetValue()) + assertEquals( + "shortcuts_library", + TopSites.openDefault.testGetValue()!!.single().extra!!["source"], + ) + } + @Test fun `GIVEN existing tab for url WHEN Default TopSite selected THEN open new tab`() = runTest { val url = "mozilla.org" @@ -253,11 +280,11 @@ class DefaultTopSiteControllerTest { assertNotNull(TopSites.openInNewTab.testGetValue()) assertEquals(1, TopSites.openInNewTab.testGetValue()!!.size) - assertNull(TopSites.openInNewTab.testGetValue()!!.single().extra) + assertEquals("homepage", TopSites.openInNewTab.testGetValue()!!.single().extra!!["source"]) assertNotNull(TopSites.openDefault.testGetValue()) assertEquals(1, TopSites.openDefault.testGetValue()!!.size) - assertNull(TopSites.openDefault.testGetValue()!!.single().extra) + assertEquals("homepage", TopSites.openDefault.testGetValue()!!.single().extra!!["source"]) verify { tabsUseCases.addTab.invoke( @@ -301,11 +328,11 @@ class DefaultTopSiteControllerTest { assertNotNull(TopSites.openInNewTab.testGetValue()) assertEquals(1, TopSites.openInNewTab.testGetValue()!!.size) - assertNull(TopSites.openInNewTab.testGetValue()!!.single().extra) + assertEquals("homepage", TopSites.openInNewTab.testGetValue()!!.single().extra!!["source"]) assertNotNull(TopSites.openContileTopSite.testGetValue()) assertEquals(1, TopSites.openContileTopSite.testGetValue()!!.size) - assertNull(TopSites.openContileTopSite.testGetValue()!!.single().extra) + assertEquals("homepage", TopSites.openContileTopSite.testGetValue()!!.single().extra!!["source"]) verify { tabsUseCases.addTab.invoke( @@ -348,7 +375,7 @@ class DefaultTopSiteControllerTest { assertNotNull(TopSites.openFrecency.testGetValue()) assertEquals(1, TopSites.openFrecency.testGetValue()!!.size) - assertNull(TopSites.openFrecency.testGetValue()!!.single().extra) + assertEquals("homepage", TopSites.openFrecency.testGetValue()!!.single().extra!!["source"]) verify { selectTabUseCase.invoke(existingTabForUrl.id) @@ -386,7 +413,7 @@ class DefaultTopSiteControllerTest { assertNotNull(TopSites.openPinned.testGetValue()) assertEquals(1, TopSites.openPinned.testGetValue()!!.size) - assertNull(TopSites.openPinned.testGetValue()!!.single().extra) + assertEquals("homepage", TopSites.openPinned.testGetValue()!!.single().extra!!["source"]) verify { selectTabUseCase.invoke(existingTabForUrl.id) @@ -412,15 +439,18 @@ class DefaultTopSiteControllerTest { assertNotNull(TopSites.openInNewTab.testGetValue()) assertEquals(1, TopSites.openInNewTab.testGetValue()!!.size) - assertNull(TopSites.openInNewTab.testGetValue()!!.single().extra) + assertEquals("homepage", TopSites.openInNewTab.testGetValue()!!.single().extra!!["source"]) assertNotNull(TopSites.openDefault.testGetValue()) assertEquals(1, TopSites.openDefault.testGetValue()!!.size) - assertNull(TopSites.openDefault.testGetValue()!!.single().extra) + assertEquals("homepage", TopSites.openDefault.testGetValue()!!.single().extra!!["source"]) assertNotNull(TopSites.openGoogleSearchAttribution.testGetValue()) assertEquals(1, TopSites.openGoogleSearchAttribution.testGetValue()!!.size) - assertNull(TopSites.openGoogleSearchAttribution.testGetValue()!!.single().extra) + assertEquals( + "homepage", + TopSites.openGoogleSearchAttribution.testGetValue()!!.single().extra!!["source"], + ) verify { tabsUseCases.addTab.invoke( @@ -450,15 +480,18 @@ class DefaultTopSiteControllerTest { assertNotNull(TopSites.openInNewTab.testGetValue()) assertEquals(1, TopSites.openInNewTab.testGetValue()!!.size) - assertNull(TopSites.openInNewTab.testGetValue()!!.single().extra) + assertEquals("homepage", TopSites.openInNewTab.testGetValue()!!.single().extra!!["source"]) assertNotNull(TopSites.openDefault.testGetValue()) assertEquals(1, TopSites.openDefault.testGetValue()!!.size) - assertNull(TopSites.openDefault.testGetValue()!!.single().extra) + assertEquals("homepage", TopSites.openDefault.testGetValue()!!.single().extra!!["source"]) assertNotNull(TopSites.openGoogleSearchAttribution.testGetValue()) assertEquals(1, TopSites.openGoogleSearchAttribution.testGetValue()!!.size) - assertNull(TopSites.openGoogleSearchAttribution.testGetValue()!!.single().extra) + assertEquals( + "homepage", + TopSites.openGoogleSearchAttribution.testGetValue()!!.single().extra!!["source"], + ) verify { tabsUseCases.addTab.invoke( @@ -490,11 +523,14 @@ class DefaultTopSiteControllerTest { assertNotNull(TopSites.openDefault.testGetValue()) assertEquals(1, TopSites.openDefault.testGetValue()!!.size) - assertNull(TopSites.openDefault.testGetValue()!!.single().extra) + assertEquals("homepage", TopSites.openDefault.testGetValue()!!.single().extra!!["source"]) assertNotNull(TopSites.openGoogleSearchAttribution.testGetValue()) assertEquals(1, TopSites.openGoogleSearchAttribution.testGetValue()!!.size) - assertNull(TopSites.openGoogleSearchAttribution.testGetValue()!!.single().extra) + assertEquals( + "homepage", + TopSites.openGoogleSearchAttribution.testGetValue()!!.single().extra!!["source"], + ) } @Test @@ -537,15 +573,18 @@ class DefaultTopSiteControllerTest { assertNotNull(TopSites.openInNewTab.testGetValue()) assertEquals(1, TopSites.openInNewTab.testGetValue()!!.size) - assertNull(TopSites.openInNewTab.testGetValue()!!.single().extra) + assertEquals("homepage", TopSites.openInNewTab.testGetValue()!!.single().extra!!["source"]) assertNotNull(TopSites.openPinned.testGetValue()) assertEquals(1, TopSites.openPinned.testGetValue()!!.size) - assertNull(TopSites.openPinned.testGetValue()!!.single().extra) + assertEquals("homepage", TopSites.openPinned.testGetValue()!!.single().extra!!["source"]) assertNotNull(TopSites.openGoogleSearchAttribution.testGetValue()) assertEquals(1, TopSites.openGoogleSearchAttribution.testGetValue()!!.size) - assertNull(TopSites.openGoogleSearchAttribution.testGetValue()!!.single().extra) + assertEquals( + "homepage", + TopSites.openGoogleSearchAttribution.testGetValue()!!.single().extra!!["source"], + ) verify { tabsUseCases.addTab.invoke( @@ -575,15 +614,18 @@ class DefaultTopSiteControllerTest { assertNotNull(TopSites.openInNewTab.testGetValue()) assertEquals(1, TopSites.openInNewTab.testGetValue()!!.size) - assertNull(TopSites.openInNewTab.testGetValue()!!.single().extra) + assertEquals("homepage", TopSites.openInNewTab.testGetValue()!!.single().extra!!["source"]) assertNotNull(TopSites.openPinned.testGetValue()) assertEquals(1, TopSites.openPinned.testGetValue()!!.size) - assertNull(TopSites.openPinned.testGetValue()!!.single().extra) + assertEquals("homepage", TopSites.openPinned.testGetValue()!!.single().extra!!["source"]) assertNotNull(TopSites.openGoogleSearchAttribution.testGetValue()) assertEquals(1, TopSites.openGoogleSearchAttribution.testGetValue()!!.size) - assertNull(TopSites.openGoogleSearchAttribution.testGetValue()!!.single().extra) + assertEquals( + "homepage", + TopSites.openGoogleSearchAttribution.testGetValue()!!.single().extra!!["source"], + ) verify { tabsUseCases.addTab.invoke( @@ -613,15 +655,18 @@ class DefaultTopSiteControllerTest { assertNotNull(TopSites.openInNewTab.testGetValue()) assertEquals(1, TopSites.openInNewTab.testGetValue()!!.size) - assertNull(TopSites.openInNewTab.testGetValue()!!.single().extra) + assertEquals("homepage", TopSites.openInNewTab.testGetValue()!!.single().extra!!["source"]) assertNotNull(TopSites.openFrecency.testGetValue()) assertEquals(1, TopSites.openFrecency.testGetValue()!!.size) - assertNull(TopSites.openFrecency.testGetValue()!!.single().extra) + assertEquals("homepage", TopSites.openFrecency.testGetValue()!!.single().extra!!["source"]) assertNotNull(TopSites.openGoogleSearchAttribution.testGetValue()) assertEquals(1, TopSites.openGoogleSearchAttribution.testGetValue()!!.size) - assertNull(TopSites.openGoogleSearchAttribution.testGetValue()!!.single().extra) + assertEquals( + "homepage", + TopSites.openGoogleSearchAttribution.testGetValue()!!.single().extra!!["source"], + ) verify { tabsUseCases.addTab.invoke( @@ -651,15 +696,18 @@ class DefaultTopSiteControllerTest { assertNotNull(TopSites.openInNewTab.testGetValue()) assertEquals(1, TopSites.openInNewTab.testGetValue()!!.size) - assertNull(TopSites.openInNewTab.testGetValue()!!.single().extra) + assertEquals("homepage", TopSites.openInNewTab.testGetValue()!!.single().extra!!["source"]) assertNotNull(TopSites.openFrecency.testGetValue()) assertEquals(1, TopSites.openFrecency.testGetValue()!!.size) - assertNull(TopSites.openFrecency.testGetValue()!!.single().extra) + assertEquals("homepage", TopSites.openFrecency.testGetValue()!!.single().extra!!["source"]) assertNotNull(TopSites.openGoogleSearchAttribution.testGetValue()) assertEquals(1, TopSites.openGoogleSearchAttribution.testGetValue()!!.size) - assertNull(TopSites.openGoogleSearchAttribution.testGetValue()!!.single().extra) + assertEquals( + "homepage", + TopSites.openGoogleSearchAttribution.testGetValue()!!.single().extra!!["source"], + ) verify { tabsUseCases.addTab.invoke( @@ -691,11 +739,11 @@ class DefaultTopSiteControllerTest { assertNotNull(TopSites.openInNewTab.testGetValue()) assertEquals(1, TopSites.openInNewTab.testGetValue()!!.size) - assertNull(TopSites.openInNewTab.testGetValue()!!.single().extra) + assertEquals("homepage", TopSites.openInNewTab.testGetValue()!!.single().extra!!["source"]) assertNotNull(TopSites.openContileTopSite.testGetValue()) assertEquals(1, TopSites.openContileTopSite.testGetValue()!!.size) - assertNull(TopSites.openContileTopSite.testGetValue()!!.single().extra) + assertEquals("homepage", TopSites.openContileTopSite.testGetValue()!!.single().extra!!["source"]) verify { tabsUseCases.addTab.invoke( @@ -746,7 +794,7 @@ class DefaultTopSiteControllerTest { assertEquals("top_sites", event[0].category) assertEquals("contile_click", event[0].name) assertEquals("1", event[0].extra!!["position"]) - assertEquals("newtab", event[0].extra!!["source"]) + assertEquals("homepage", event[0].extra!!["source"]) job.join() assertTrue(topSiteImpressionPinged) @@ -792,7 +840,7 @@ class DefaultTopSiteControllerTest { assertEquals("top_sites", event[0].category) assertEquals("contile_click", event[0].name) assertEquals("1", event[0].extra!!["position"]) - assertEquals("newtab", event[0].extra!!["source"]) + assertEquals("homepage", event[0].extra!!["source"]) job.join() assertTrue(topSiteImpressionPinged) @@ -836,7 +884,7 @@ class DefaultTopSiteControllerTest { assertEquals("top_sites", event[0].category) assertEquals("contile_impression", event[0].name) assertEquals("1", event[0].extra!!["position"]) - assertEquals("newtab", event[0].extra!!["source"]) + assertEquals("homepage", event[0].extra!!["source"]) job.join() assertTrue(topSiteImpressionSubmitted) @@ -882,12 +930,58 @@ class DefaultTopSiteControllerTest { assertEquals("top_sites", event[0].category) assertEquals("contile_impression", event[0].name) assertEquals("1", event[0].extra!!["position"]) - assertEquals("newtab", event[0].extra!!["source"]) + assertEquals("homepage", event[0].extra!!["source"]) job.join() assertTrue(topSiteImpressionSubmitted) } + @Test + fun `GIVEN the shortcuts library source WHEN a provided top site is clicked THEN record contile_click with the shortcuts library source`() = runTest { + val controller = spyk(createController(this, source = TopSitesSource.SHORTCUTS_LIBRARY)) + val topSite = TopSite.Provided( + id = 3, + title = "Mozilla", + url = "https://mozilla.com", + clickUrl = "https://mozilla.com/click", + imageUrl = "https://test.com/image2.jpg", + impressionUrl = "https://mozilla.com/impression", + createdAt = 3, + ) + + every { controller.getAvailableSearchEngines() } returns listOf(searchEngine) + + assertNull(TopSites.contileClick.testGetValue()) + + controller.handleSelectTopSite(topSite, position = 0) + + val event = TopSites.contileClick.testGetValue()!! + assertEquals(1, event.size) + assertEquals("shortcuts_library", event[0].extra!!["source"]) + } + + @Test + fun `GIVEN the shortcuts library source WHEN a provided top site is seen THEN record contile_impression with the shortcuts library source`() = runTest { + val controller = spyk(createController(this, source = TopSitesSource.SHORTCUTS_LIBRARY)) + val topSite = TopSite.Provided( + id = 3, + title = "Mozilla", + url = "https://mozilla.com", + clickUrl = "https://mozilla.com/click", + imageUrl = "https://test.com/image2.jpg", + impressionUrl = "https://mozilla.com/impression", + createdAt = 3, + ) + + assertNull(TopSites.contileImpression.testGetValue()) + + controller.handleTopSiteImpression(topSite, position = 0) + + val event = TopSites.contileImpression.testGetValue()!! + assertEquals(1, event.size) + assertEquals("shortcuts_library", event[0].extra!!["source"]) + } + @Test fun `WHEN the default Google top site is removed THEN the correct metric is recorded`() = runTest { val controller = spyk(createController(this)) @@ -904,11 +998,14 @@ class DefaultTopSiteControllerTest { assertNotNull(TopSites.googleTopSiteRemoved.testGetValue()) assertEquals(1, TopSites.googleTopSiteRemoved.testGetValue()!!.size) - assertNull(TopSites.googleTopSiteRemoved.testGetValue()!!.single().extra) + assertEquals( + "homepage", + TopSites.googleTopSiteRemoved.testGetValue()!!.single().extra!!["source"], + ) assertNotNull(TopSites.remove.testGetValue()) assertEquals(1, TopSites.remove.testGetValue()!!.size) - assertNull(TopSites.remove.testGetValue()!!.single().extra) + assertEquals("homepage", TopSites.remove.testGetValue()!!.single().extra!!["source"]) } @Test @@ -971,7 +1068,7 @@ class DefaultTopSiteControllerTest { assertNotNull(TopSites.contileSettings.testGetValue()) assertEquals(1, TopSites.contileSettings.testGetValue()!!.size) - assertNull(TopSites.contileSettings.testGetValue()!!.single().extra) + assertEquals("homepage", TopSites.contileSettings.testGetValue()!!.single().extra!!["source"]) verify { navController.navigate(R.id.homeSettingsFragment) } } @@ -982,7 +1079,10 @@ class DefaultTopSiteControllerTest { assertNotNull(TopSites.contileSponsorsAndPrivacy.testGetValue()) assertEquals(1, TopSites.contileSponsorsAndPrivacy.testGetValue()!!.size) - assertNull(TopSites.contileSponsorsAndPrivacy.testGetValue()!!.single().extra) + assertEquals( + "homepage", + TopSites.contileSponsorsAndPrivacy.testGetValue()!!.single().extra!!["source"], + ) verify { navController.navigate(R.id.browserFragment) @@ -1000,11 +1100,14 @@ class DefaultTopSiteControllerTest { every { id } returns R.id.shortcutsFragment } - createController(this).handleSponsorPrivacyClicked() + createController(this, source = TopSitesSource.SHORTCUTS_LIBRARY).handleSponsorPrivacyClicked() assertNotNull(TopSites.contileSponsorsAndPrivacy.testGetValue()) assertEquals(1, TopSites.contileSponsorsAndPrivacy.testGetValue()!!.size) - assertNull(TopSites.contileSponsorsAndPrivacy.testGetValue()!!.single().extra) + assertEquals( + "shortcuts_library", + TopSites.contileSponsorsAndPrivacy.testGetValue()!!.single().extra!!["source"], + ) verify { navController.navigate(ShortcutsFragmentDirections.actionShortcutsFragmentToBrowserFragment()) @@ -1033,6 +1136,7 @@ class DefaultTopSiteControllerTest { createController(this).handleTopSiteLongClicked(topSite) assertEquals(topSite.type, TopSites.longPress.testGetValue()!!.single().extra!!["type"]) + assertEquals("homepage", TopSites.longPress.testGetValue()!!.single().extra!!["source"]) } @Test @@ -1050,7 +1154,10 @@ class DefaultTopSiteControllerTest { assertNotNull(TopSites.openContileInPrivateTab.testGetValue()) assertEquals(1, TopSites.openContileInPrivateTab.testGetValue()!!.size) - assertNull(TopSites.openContileInPrivateTab.testGetValue()!!.single().extra) + assertEquals( + "homepage", + TopSites.openContileInPrivateTab.testGetValue()!!.single().extra!!["source"], + ) verify { navController.navigate(R.id.browserFragment) @@ -1077,11 +1184,14 @@ class DefaultTopSiteControllerTest { impressionUrl = "", createdAt = 0, ) - createController(this).handleOpenInPrivateTabClicked(topSite) + createController(this, source = TopSitesSource.SHORTCUTS_LIBRARY).handleOpenInPrivateTabClicked(topSite) assertNotNull(TopSites.openContileInPrivateTab.testGetValue()) assertEquals(1, TopSites.openContileInPrivateTab.testGetValue()!!.size) - assertNull(TopSites.openContileInPrivateTab.testGetValue()!!.single().extra) + assertEquals( + "shortcuts_library", + TopSites.openContileInPrivateTab.testGetValue()!!.single().extra!!["source"], + ) verify { navController.navigate(ShortcutsFragmentDirections.actionShortcutsFragmentToBrowserFragment()) @@ -1110,7 +1220,10 @@ class DefaultTopSiteControllerTest { assertNotNull(TopSites.openContileInPrivateTab.testGetValue()) assertEquals(1, TopSites.openContileInPrivateTab.testGetValue()!!.size) - assertNull(TopSites.openContileInPrivateTab.testGetValue()!!.single().extra) + assertEquals( + "homepage", + TopSites.openContileInPrivateTab.testGetValue()!!.single().extra!!["source"], + ) verify { navController.navigate(R.id.browserFragment) @@ -1152,7 +1265,7 @@ class DefaultTopSiteControllerTest { assertNotNull(TopSites.openInPrivateTab.testGetValue()) assertEquals(3, TopSites.openInPrivateTab.testGetValue()!!.size) for (event in TopSites.openInPrivateTab.testGetValue()!!) { - assertNull(event.extra) + assertEquals("homepage", event.extra!!["source"]) } } @@ -1163,7 +1276,10 @@ class DefaultTopSiteControllerTest { assertNotNull(ShortcutsLibrary.viewed.testGetValue()) } - private fun createController(scope: CoroutineScope): DefaultTopSiteController = + private fun createController( + scope: CoroutineScope, + source: TopSitesSource = TopSitesSource.HOMEPAGE, + ): DefaultTopSiteController = DefaultTopSiteController( activityRef = WeakReference(activity), navControllerRef = WeakReference(navController), @@ -1176,5 +1292,6 @@ class DefaultTopSiteControllerTest { marsUseCases = marsUseCases, mozAdsUseCases = mozAdsUseCases, viewLifecycleScope = scope, + source = source, ) }