From 56022546f2466cb9ddd796538f993eb2c78d55f8 Mon Sep 17 00:00:00 2001 From: Gabriel Luong Date: Wed, 23 Feb 2022 13:52:17 -0500 Subject: [PATCH] For #23893 - Add telemetry for Contile services --- app/metrics.yaml | 137 +++++++++++++++++- app/pings.yaml | 13 ++ .../org/mozilla/fenix/FenixApplication.kt | 8 + .../mozilla/fenix/components/metrics/Event.kt | 8 + .../components/metrics/GleanMetricsService.kt | 20 +++ .../SessionControlController.kt | 25 +++- .../SessionControlInteractor.kt | 7 +- .../home/topsites/TopSiteItemViewHolder.kt | 29 +++- .../fenix/home/topsites/TopSitesAdapter.kt | 4 +- .../java/org/mozilla/fenix/utils/Settings.kt | 5 + app/src/main/res/values/preference_keys.xml | 2 + .../org/mozilla/fenix/FenixApplicationTest.kt | 18 ++- .../metrics/GleanMetricsServiceTest.kt | 42 ++++++ .../DefaultSessionControlControllerTest.kt | 57 ++++++-- .../topsites/TopSiteItemViewHolderTest.kt | 49 ++++++- 15 files changed, 392 insertions(+), 32 deletions(-) diff --git a/app/metrics.yaml b/app/metrics.yaml index a149576ae..1af111604 100644 --- a/app/metrics.yaml +++ b/app/metrics.yaml @@ -4786,6 +4786,7 @@ voice_search: tags: - Search - Voice + top_sites: open_default: type: event @@ -5088,6 +5089,141 @@ top_sites: notification_emails: - android-probes@mozilla.com expires: 111 + context_id: + type: uuid + description: | + A UUID that is unjoinable with other browser metrics. This ID will not be + shared with AdM, only for internal uses. This ID is shared across all + contextual services features. + lifetime: ping + send_in_pings: + - topsites-impression + bugs: + - https://github.com/mozilla-mobile/fenix/issues/23893 + data_reviews: + - https://github.com/mozilla-mobile/fenix/pull/23945 + data_sensitivity: + - highly_sensitive + notification_emails: + - android-probes@mozilla.com + expires: 112 + metadata: + tags: + - TopSites + contile_tile_id: + type: quantity + description: | + A unique identifier provided by the AdM for the sponsored TopSites tile + lifetime: ping + send_in_pings: + - topsites-impression + bugs: + - https://github.com/mozilla-mobile/fenix/issues/23893 + data_reviews: + - https://github.com/mozilla-mobile/fenix/pull/23945 + data_sensitivity: + - technical + - interaction + notification_emails: + - android-probes@mozilla.com + expires: 112 + unit: integer + metadata: + tags: + - TopSites + contile_advertiser: + type: string + description: | + Advertiser brand for the sponsored TopSites tile + lifetime: ping + send_in_pings: + - topsites-impression + bugs: + - https://github.com/mozilla-mobile/fenix/issues/23893 + data_reviews: + - https://github.com/mozilla-mobile/fenix/pull/23945 + data_sensitivity: + - technical + - interaction + notification_emails: + - android-probes@mozilla.com + expires: 112 + metadata: + tags: + - TopSites + contile_reporting_url: + type: url + description: | + The AdM reporting endpoint (impression_url for “impression” event, + click_url for “click” event). + lifetime: ping + send_in_pings: + - topsites-impression + bugs: + - https://github.com/mozilla-mobile/fenix/issues/23893 + data_reviews: + - https://github.com/mozilla-mobile/fenix/pull/23945 + data_sensitivity: + - technical + - interaction + notification_emails: + - android-probes@mozilla.com + expires: 112 + metadata: + tags: + - TopSites + contile_impression: + type: event + description: | + A user saw a Contile top site + extra_keys: + source: + description: The source of the event, example "newtab", "urlbar" + type: string + position: + description: The tile placement (1-based) + type: quantity + send_in_pings: + - topsites-impression + - events + bugs: + - https://github.com/mozilla-mobile/fenix/issues/23893 + data_reviews: + - https://github.com/mozilla-mobile/fenix/pull/23945 + data_sensitivity: + - interaction + notification_emails: + - android-probes@mozilla.com + expires: 112 + metadata: + tags: + - TopSites + contile_click: + type: event + description: | + A user clicked a Contile top site + extra_keys: + source: + description: The source of the event, example "newtab", "urlbar" + type: string + position: + description: The tile placement (1-based) + type: quantity + send_in_pings: + - topsites-impression + - events + bugs: + - https://github.com/mozilla-mobile/fenix/issues/23893 + data_reviews: + - https://github.com/mozilla-mobile/fenix/pull/23945 + data_sensitivity: + - interaction + notification_emails: + - android-probes@mozilla.com + expires: 112 + metadata: + tags: + - TopSites app_theme: dark_theme_selected: @@ -5113,7 +5249,6 @@ app_theme: notification_emails: - android-probes@mozilla.com expires: 108 - metadata: tags: - Themes diff --git a/app/pings.yaml b/app/pings.yaml index 34b33a968..ef205112c 100644 --- a/app/pings.yaml +++ b/app/pings.yaml @@ -30,3 +30,16 @@ first-session: - https://github.com/mozilla-mobile/fenix/pull/8074#issuecomment-586512202 notification_emails: - android-probes@mozilla.com + +topsites-impression: + description: | + Recorded when a sponsored top site is rendered and visible on the home + screen. Visibility is qualified as when the homepage is brought to the + front of the Browser, and sponsored tiles are 100% visible on screen. + include_client_id: false + bugs: + - https://github.com/mozilla-mobile/fenix/issues/23893 + data_reviews: + - https://github.com/mozilla-mobile/fenix/pull/23945 + notification_emails: + - android-probes@mozilla.com diff --git a/app/src/main/java/org/mozilla/fenix/FenixApplication.kt b/app/src/main/java/org/mozilla/fenix/FenixApplication.kt index 0d8cfa552..5ed920a82 100644 --- a/app/src/main/java/org/mozilla/fenix/FenixApplication.kt +++ b/app/src/main/java/org/mozilla/fenix/FenixApplication.kt @@ -62,6 +62,7 @@ import org.mozilla.fenix.GleanMetrics.Metrics import org.mozilla.fenix.GleanMetrics.PerfStartup import org.mozilla.fenix.GleanMetrics.Preferences import org.mozilla.fenix.GleanMetrics.SearchDefaultEngine +import org.mozilla.fenix.GleanMetrics.TopSites import org.mozilla.fenix.components.Components import org.mozilla.fenix.components.Core import org.mozilla.fenix.components.metrics.Event @@ -84,6 +85,7 @@ import org.mozilla.fenix.telemetry.TelemetryLifecycleObserver import org.mozilla.fenix.utils.BrowsersCache import org.mozilla.fenix.utils.Settings import org.mozilla.fenix.utils.Settings.Companion.TOP_SITES_PROVIDER_MAX_THRESHOLD +import java.util.UUID import java.util.concurrent.TimeUnit /** @@ -578,6 +580,12 @@ open class FenixApplication : LocaleAwareApplication(), Provider { defaultMozBrowser.set(it) } + if (settings.contileContextId.isEmpty()) { + settings.contileContextId = TopSites.contextId.generateAndSet().toString() + } else { + TopSites.contextId.set(UUID.fromString(settings.contileContextId)) + } + mozillaProducts.set(mozillaProductDetector.getInstalledMozillaProducts(applicationContext)) adjustCampaign.set(settings.adjustCampaignId) diff --git a/app/src/main/java/org/mozilla/fenix/components/metrics/Event.kt b/app/src/main/java/org/mozilla/fenix/components/metrics/Event.kt index 91b6a443d..4da88d28e 100644 --- a/app/src/main/java/org/mozilla/fenix/components/metrics/Event.kt +++ b/app/src/main/java/org/mozilla/fenix/components/metrics/Event.kt @@ -331,6 +331,14 @@ sealed class Event { get() = hashMapOf(TopSites.longPressKeys.type to topSite.name()) } + data class TopSiteContileImpression(val position: Int, val source: Source) : Event() { + enum class Source { NEWTAB } + } + + data class TopSiteContileClick(val position: Int, val source: Source) : Event() { + enum class Source { NEWTAB } + } + data class OnboardingToolbarPosition(val position: Position) : Event() { enum class Position { TOP, BOTTOM } diff --git a/app/src/main/java/org/mozilla/fenix/components/metrics/GleanMetricsService.kt b/app/src/main/java/org/mozilla/fenix/components/metrics/GleanMetricsService.kt index a141fc8a2..ba840dfdc 100644 --- a/app/src/main/java/org/mozilla/fenix/components/metrics/GleanMetricsService.kt +++ b/app/src/main/java/org/mozilla/fenix/components/metrics/GleanMetricsService.kt @@ -558,6 +558,26 @@ private val Event.wrapper: EventWrapper<*>? { TopSites.swipeCarousel.record(it) }, { TopSites.swipeCarouselKeys.valueOf(it) } ) + is Event.TopSiteContileImpression -> EventWrapper( + { + TopSites.contileImpression.record( + TopSites.ContileImpressionExtra( + position = this.position, + source = this.source.name.lowercase() + ) + ) + } + ) + is Event.TopSiteContileClick -> EventWrapper( + { + TopSites.contileClick.record( + TopSites.ContileClickExtra( + position = this.position, + source = this.source.name.lowercase() + ) + ) + } + ) is Event.PocketTopSiteClicked -> EventWrapper( { Pocket.pocketTopSiteClicked.record(it) } ) diff --git a/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/SessionControlController.kt b/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/SessionControlController.kt index b0177797b..c31239918 100644 --- a/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/SessionControlController.kt +++ b/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/SessionControlController.kt @@ -29,6 +29,8 @@ import mozilla.components.support.ktx.android.view.showKeyboard import mozilla.components.support.ktx.kotlin.isUrl import org.mozilla.fenix.BrowserDirection import org.mozilla.fenix.FeatureFlags +import org.mozilla.fenix.GleanMetrics.Pings +import org.mozilla.fenix.GleanMetrics.TopSites import org.mozilla.fenix.HomeActivity import org.mozilla.fenix.R import org.mozilla.fenix.browser.BrowserFragmentDirections @@ -119,7 +121,7 @@ interface SessionControlController { /** * @see [TopSiteInteractor.onSelectTopSite] */ - fun handleSelectTopSite(topSite: TopSite) + fun handleSelectTopSite(topSite: TopSite, position: Int) /** * @see [TopSiteInteractor.onSettingsClicked] @@ -391,7 +393,7 @@ class DefaultSessionControlController( metrics.track(Event.CollectionRenamePressed) } - override fun handleSelectTopSite(topSite: TopSite) { + override fun handleSelectTopSite(topSite: TopSite, position: Int) { dismissSearchDialogIfDisplayed() metrics.track(Event.TopSiteOpenInNewTab) @@ -401,7 +403,9 @@ class DefaultSessionControlController( is TopSite.Default -> Event.TopSiteOpenDefault is TopSite.Frecent -> Event.TopSiteOpenFrecent is TopSite.Pinned -> Event.TopSiteOpenPinned - is TopSite.Provided -> Event.TopSiteOpenProvided + is TopSite.Provided -> Event.TopSiteOpenProvided.also { + submitTopSitesImpressionPing(topSite, position) + } } ) @@ -435,6 +439,21 @@ class DefaultSessionControlController( activity.openToBrowser(BrowserDirection.FromHome) } + @VisibleForTesting + internal fun submitTopSitesImpressionPing(topSite: TopSite.Provided, position: Int) { + metrics.track( + Event.TopSiteContileClick( + position = position + 1, + source = Event.TopSiteContileClick.Source.NEWTAB + ) + ) + + topSite.id?.let { TopSites.contileTileId.set(it) } + topSite.title?.let { TopSites.contileAdvertiser.set(it.lowercase()) } + TopSites.contileReportingUrl.set(topSite.clickUrl) + Pings.topsitesImpression.submit() + } + override fun handleTopSiteSettingsClicked() { metrics.track(Event.TopSiteContileSettings) navController.nav( diff --git a/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/SessionControlInteractor.kt b/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/SessionControlInteractor.kt index 69462d767..ed197eb7d 100644 --- a/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/SessionControlInteractor.kt +++ b/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/SessionControlInteractor.kt @@ -211,8 +211,9 @@ interface TopSiteInteractor { * Selects the given top site. Called when a user clicks on a top site. * * @param topSite The top site that was selected. + * @param position The position of the top site. */ - fun onSelectTopSite(topSite: TopSite) + fun onSelectTopSite(topSite: TopSite, position: Int) /** * Navigates to the Homepage Settings. Called when an user clicks on the "Settings" top site @@ -310,8 +311,8 @@ class SessionControlInteractor( controller.handleRenameCollectionTapped(collection) } - override fun onSelectTopSite(topSite: TopSite) { - controller.handleSelectTopSite(topSite) + override fun onSelectTopSite(topSite: TopSite, position: Int) { + controller.handleSelectTopSite(topSite, position) } override fun onSettingsClicked() { diff --git a/app/src/main/java/org/mozilla/fenix/home/topsites/TopSiteItemViewHolder.kt b/app/src/main/java/org/mozilla/fenix/home/topsites/TopSiteItemViewHolder.kt index a30d57c84..15f3a34a4 100644 --- a/app/src/main/java/org/mozilla/fenix/home/topsites/TopSiteItemViewHolder.kt +++ b/app/src/main/java/org/mozilla/fenix/home/topsites/TopSiteItemViewHolder.kt @@ -8,6 +8,7 @@ import android.annotation.SuppressLint import android.view.MotionEvent import android.view.View import android.widget.PopupWindow +import androidx.annotation.VisibleForTesting import androidx.appcompat.content.res.AppCompatResources.getDrawable import androidx.core.view.isVisible import androidx.lifecycle.LifecycleOwner @@ -17,6 +18,8 @@ import kotlinx.coroutines.Dispatchers.Main import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import mozilla.components.feature.top.sites.TopSite +import org.mozilla.fenix.GleanMetrics.Pings +import org.mozilla.fenix.GleanMetrics.TopSites import org.mozilla.fenix.R import org.mozilla.fenix.components.metrics.Event import org.mozilla.fenix.databinding.TopSiteItemBinding @@ -36,10 +39,6 @@ class TopSiteItemViewHolder( private val binding = TopSiteItemBinding.bind(view) init { - binding.topSiteItem.setOnClickListener { - interactor.onSelectTopSite(topSite) - } - binding.topSiteItem.setOnLongClickListener { interactor.onTopSiteMenuOpened() it.context.components.analytics.metrics.track(Event.TopSiteLongPress(topSite)) @@ -72,7 +71,11 @@ class TopSiteItemViewHolder( } } - fun bind(topSite: TopSite) { + fun bind(topSite: TopSite, position: Int) { + binding.topSiteItem.setOnClickListener { + interactor.onSelectTopSite(topSite, position) + } + binding.topSiteTitle.text = topSite.title if (topSite is TopSite.Pinned || topSite is TopSite.Default) { @@ -89,6 +92,7 @@ class TopSiteItemViewHolder( itemView.context.components.core.client.bitmapForUrl(topSite.imageUrl)?.let { bitmap -> withContext(Main) { binding.faviconImage.setImageBitmap(bitmap) + submitTopSitesImpressionPing(topSite, position) } } } @@ -121,6 +125,21 @@ class TopSiteItemViewHolder( this.topSite = topSite } + @VisibleForTesting + internal fun submitTopSitesImpressionPing(topSite: TopSite.Provided, position: Int) { + itemView.context.components.analytics.metrics.track( + Event.TopSiteContileImpression( + position = position + 1, + source = Event.TopSiteContileImpression.Source.NEWTAB + ) + ) + + topSite.id?.let { TopSites.contileTileId.set(it) } + topSite.title?.let { TopSites.contileAdvertiser.set(it.lowercase()) } + TopSites.contileReportingUrl.set(topSite.impressionUrl) + Pings.topsitesImpression.submit() + } + private fun onTouchEvent( v: View, event: MotionEvent, diff --git a/app/src/main/java/org/mozilla/fenix/home/topsites/TopSitesAdapter.kt b/app/src/main/java/org/mozilla/fenix/home/topsites/TopSitesAdapter.kt index 85a11b3e3..e83a4854a 100644 --- a/app/src/main/java/org/mozilla/fenix/home/topsites/TopSitesAdapter.kt +++ b/app/src/main/java/org/mozilla/fenix/home/topsites/TopSitesAdapter.kt @@ -25,7 +25,7 @@ class TopSitesAdapter( override fun onBindViewHolder(holder: TopSiteItemViewHolder, position: Int) { StartupTimeline.onTopSitesItemBound(holder) - holder.bind(getItem(position)) + holder.bind(getItem(position), position) } override fun onBindViewHolder( @@ -38,7 +38,7 @@ class TopSitesAdapter( } else { when (payloads[0]) { is TopSite -> { - holder.bind((payloads[0] as TopSite)) + holder.bind((payloads[0] as TopSite), position) } } } diff --git a/app/src/main/java/org/mozilla/fenix/utils/Settings.kt b/app/src/main/java/org/mozilla/fenix/utils/Settings.kt index 602704c64..c5709dae1 100644 --- a/app/src/main/java/org/mozilla/fenix/utils/Settings.kt +++ b/app/src/main/java/org/mozilla/fenix/utils/Settings.kt @@ -176,6 +176,11 @@ class Settings(private val appContext: Context) : PreferencesHolder { default = "" ) + var contileContextId by stringPreference( + appContext.getPreferenceKey(R.string.pref_key_contile_context_id), + default = "" + ) + var currentWallpaper by stringPreference( appContext.getPreferenceKey(R.string.pref_key_current_wallpaper), default = WallpaperManager.defaultWallpaper.name diff --git a/app/src/main/res/values/preference_keys.xml b/app/src/main/res/values/preference_keys.xml index c54932adf..078548537 100644 --- a/app/src/main/res/values/preference_keys.xml +++ b/app/src/main/res/values/preference_keys.xml @@ -188,6 +188,8 @@ pref_key_adjust_adgroup pref_key_adjust_creative + pref_key_contile_context_id + pref_key_wallpapers pref_key_current_wallpaper diff --git a/app/src/test/java/org/mozilla/fenix/FenixApplicationTest.kt b/app/src/test/java/org/mozilla/fenix/FenixApplicationTest.kt index ad1cb3390..173996dca 100644 --- a/app/src/test/java/org/mozilla/fenix/FenixApplicationTest.kt +++ b/app/src/test/java/org/mozilla/fenix/FenixApplicationTest.kt @@ -19,8 +19,10 @@ import mozilla.components.concept.engine.webextension.Metadata import mozilla.components.concept.engine.webextension.WebExtension import mozilla.components.feature.addons.migration.DefaultSupportedAddonsChecker import mozilla.components.service.glean.testing.GleanTestRule +import mozilla.components.support.test.robolectric.testContext import org.junit.Assert.assertEquals import org.junit.Assert.assertFalse +import org.junit.Assert.assertTrue import org.junit.Before import org.junit.Rule import org.junit.Test @@ -29,6 +31,7 @@ import org.mozilla.fenix.GleanMetrics.Addons import org.mozilla.fenix.GleanMetrics.Metrics import org.mozilla.fenix.GleanMetrics.Preferences import org.mozilla.fenix.GleanMetrics.SearchDefaultEngine +import org.mozilla.fenix.GleanMetrics.TopSites import org.mozilla.fenix.components.metrics.MozillaProductDetector import org.mozilla.fenix.components.toolbar.ToolbarPosition import org.mozilla.fenix.helpers.FenixRobolectricTestRunner @@ -87,7 +90,7 @@ class FenixApplicationTest { fun `WHEN setStartupMetrics is called THEN sets some base metrics`() { val expectedAppName = "org.mozilla.fenix" val expectedAppInstallSource = "org.mozilla.install.source" - val settings: Settings = mockk() + val settings = spyk(Settings(testContext)) val application = spyk(application) val packageManager: PackageManager = mockk() @@ -142,6 +145,9 @@ class FenixApplicationTest { every { application.reportHomeScreenMetrics(settings) } just Runs every { settings.inactiveTabsAreEnabled } returns true + assertTrue(settings.contileContextId.isEmpty()) + assertFalse(TopSites.contextId.testHasValue()) + application.setStartupMetrics(browserStore, settings, browsersCache, mozillaProductDetector) // Verify that browser defaults metrics are set. @@ -181,10 +187,20 @@ class FenixApplicationTest { assertEquals(true, Preferences.inactiveTabsEnabled.testGetValue()) assertEquals(expectedAppInstallSource, Metrics.installSource.testGetValue()) + val contextId = TopSites.contextId.testGetValue().toString() + + assertTrue(TopSites.contextId.testHasValue()) + assertEquals(contextId, settings.contileContextId) + // Verify that search engine defaults are NOT set. This test does // not mock most of the objects telemetry is collected from. assertFalse(SearchDefaultEngine.code.testHasValue()) assertFalse(SearchDefaultEngine.name.testHasValue()) assertFalse(SearchDefaultEngine.searchUrl.testHasValue()) + + application.setStartupMetrics(browserStore, settings, browsersCache, mozillaProductDetector) + + assertEquals(contextId, TopSites.contextId.testGetValue().toString()) + assertEquals(contextId, settings.contileContextId) } } diff --git a/app/src/test/java/org/mozilla/fenix/components/metrics/GleanMetricsServiceTest.kt b/app/src/test/java/org/mozilla/fenix/components/metrics/GleanMetricsServiceTest.kt index 2cc91dfa6..95e96e810 100644 --- a/app/src/test/java/org/mozilla/fenix/components/metrics/GleanMetricsServiceTest.kt +++ b/app/src/test/java/org/mozilla/fenix/components/metrics/GleanMetricsServiceTest.kt @@ -23,6 +23,7 @@ import org.mozilla.fenix.GleanMetrics.RecentBookmarks import org.mozilla.fenix.GleanMetrics.RecentlyVisitedHomepage import org.mozilla.fenix.GleanMetrics.SyncedTabs import org.mozilla.fenix.GleanMetrics.TabsTray +import org.mozilla.fenix.GleanMetrics.TopSites import org.mozilla.fenix.helpers.FenixRobolectricTestRunner @RunWith(FenixRobolectricTestRunner::class) @@ -373,4 +374,45 @@ class GleanMetricsServiceTest { gleanService.track(Event.CreditCardManagementCardTapped) assertTrue(CreditCards.managementCardTapped.testHasValue()) } + + @Test + fun `GIVEN contile top site events WHEN the event is track THEN verify the event is correctly recorded`() { + assertFalse(TopSites.contileImpression.testHasValue()) + + gleanService.track( + Event.TopSiteContileImpression( + position = 1, + source = Event.TopSiteContileImpression.Source.NEWTAB + ) + ) + + assertTrue(TopSites.contileImpression.testHasValue()) + + var event = TopSites.contileImpression.testGetValue() + + assertEquals(1, event.size) + assertEquals("top_sites", event[0].category) + assertEquals("contile_impression", event[0].name) + assertEquals("1", event[0].extra!!["position"]) + assertEquals("newtab", event[0].extra!!["source"]) + + assertFalse(TopSites.contileClick.testHasValue()) + + gleanService.track( + Event.TopSiteContileClick( + position = 2, + source = Event.TopSiteContileClick.Source.NEWTAB + ) + ) + + assertTrue(TopSites.contileClick.testHasValue()) + + event = TopSites.contileClick.testGetValue() + + assertEquals(1, event.size) + assertEquals("top_sites", event[0].category) + assertEquals("contile_click", event[0].name) + assertEquals("2", event[0].extra!!["position"]) + assertEquals("newtab", event[0].extra!!["source"]) + } } diff --git a/app/src/test/java/org/mozilla/fenix/home/DefaultSessionControlControllerTest.kt b/app/src/test/java/org/mozilla/fenix/home/DefaultSessionControlControllerTest.kt index 7c11c6cf1..09db0e14b 100644 --- a/app/src/test/java/org/mozilla/fenix/home/DefaultSessionControlControllerTest.kt +++ b/app/src/test/java/org/mozilla/fenix/home/DefaultSessionControlControllerTest.kt @@ -43,6 +43,8 @@ import org.junit.Ignore import org.junit.Rule import org.junit.Test import org.mozilla.fenix.BrowserDirection +import org.mozilla.fenix.GleanMetrics.Pings +import org.mozilla.fenix.GleanMetrics.TopSites import org.mozilla.fenix.HomeActivity import org.mozilla.fenix.R import org.mozilla.fenix.browser.BrowserFragmentDirections @@ -399,7 +401,7 @@ class DefaultSessionControlControllerTest { every { controller.getAvailableSearchEngines() } returns listOf(searchEngine) - controller.handleSelectTopSite(topSite) + controller.handleSelectTopSite(topSite, position = 0) verify { metrics.track(Event.TopSiteOpenInNewTab) } verify { metrics.track(Event.TopSiteOpenDefault) } @@ -425,7 +427,7 @@ class DefaultSessionControlControllerTest { every { controller.getAvailableSearchEngines() } returns listOf(searchEngine) - controller.handleSelectTopSite(topSite) + controller.handleSelectTopSite(topSite, position = 0) verify { metrics.track(Event.TopSiteOpenInNewTab) } verify { @@ -452,7 +454,7 @@ class DefaultSessionControlControllerTest { store.dispatch(SearchAction.SetRegionAction(RegionState("US", "US"))).joinBlocking() - controller.handleSelectTopSite(topSite) + controller.handleSelectTopSite(topSite, position = 0) verify { metrics.track(Event.TopSiteOpenInNewTab) } verify { metrics.track(Event.TopSiteOpenDefault) } @@ -481,7 +483,7 @@ class DefaultSessionControlControllerTest { store.dispatch(SearchAction.SetRegionAction(RegionState("DE", "FR"))).joinBlocking() - controller.handleSelectTopSite(topSite) + controller.handleSelectTopSite(topSite, position = 0) verify { metrics.track(Event.TopSiteOpenInNewTab) } verify { metrics.track(Event.TopSiteOpenDefault) } @@ -514,7 +516,7 @@ class DefaultSessionControlControllerTest { every { any().selectedOrDefaultSearchEngine } returns googleSearchEngine - controller.handleSelectTopSite(topSite) + controller.handleSelectTopSite(topSite, position = 0) verify { metrics.track( @@ -550,7 +552,7 @@ class DefaultSessionControlControllerTest { every { any().selectedOrDefaultSearchEngine } returns googleSearchEngine - controller.handleSelectTopSite(topSite) + controller.handleSelectTopSite(topSite, position = 0) verify { metrics.track( @@ -582,7 +584,7 @@ class DefaultSessionControlControllerTest { store.dispatch(SearchAction.SetRegionAction(RegionState("US", "US"))).joinBlocking() - controller.handleSelectTopSite(topSite) + controller.handleSelectTopSite(topSite, position = 0) verify { metrics.track(Event.TopSiteOpenInNewTab) } verify { metrics.track(Event.TopSiteOpenPinned) } @@ -611,7 +613,7 @@ class DefaultSessionControlControllerTest { store.dispatch(SearchAction.SetRegionAction(RegionState("DE", "FR"))).joinBlocking() - controller.handleSelectTopSite(topSite) + controller.handleSelectTopSite(topSite, position = 0) verify { metrics.track(Event.TopSiteOpenInNewTab) } verify { metrics.track(Event.TopSiteOpenPinned) } @@ -640,7 +642,7 @@ class DefaultSessionControlControllerTest { store.dispatch(SearchAction.SetRegionAction(RegionState("US", "US"))).joinBlocking() - controller.handleSelectTopSite(topSite) + controller.handleSelectTopSite(topSite, position = 0) verify { metrics.track(Event.TopSiteOpenInNewTab) } verify { metrics.track(Event.TopSiteOpenFrecent) } @@ -669,7 +671,7 @@ class DefaultSessionControlControllerTest { store.dispatch(SearchAction.SetRegionAction(RegionState("DE", "FR"))).joinBlocking() - controller.handleSelectTopSite(topSite) + controller.handleSelectTopSite(topSite, position = 0) verify { metrics.track(Event.TopSiteOpenInNewTab) } verify { metrics.track(Event.TopSiteOpenFrecent) } @@ -695,11 +697,12 @@ class DefaultSessionControlControllerTest { impressionUrl = "", createdAt = 0 ) + val position = 0 val controller = spyk(createController()) every { controller.getAvailableSearchEngines() } returns listOf(searchEngine) - controller.handleSelectTopSite(topSite) + controller.handleSelectTopSite(topSite, position) verify { metrics.track(Event.TopSiteOpenInNewTab) } verify { metrics.track(Event.TopSiteOpenProvided) } @@ -710,9 +713,41 @@ class DefaultSessionControlControllerTest { startLoading = true ) } + verify { controller.submitTopSitesImpressionPing(topSite, position) } verify { activity.openToBrowser(BrowserDirection.FromHome) } } + @Test + fun `GIVEN a provided top site WHEN the provided top site is clicked THEN submit a top site impression ping`() { + val controller = spyk(createController()) + 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://example.com", + createdAt = 3 + ) + val position = 0 + + controller.submitTopSitesImpressionPing(topSite, position) + + verify { + metrics.track( + Event.TopSiteContileClick( + position = position + 1, + source = Event.TopSiteContileClick.Source.NEWTAB + ) + ) + + TopSites.contileTileId.set(3) + TopSites.contileAdvertiser.set("mozilla") + TopSites.contileReportingUrl.set(topSite.clickUrl) + Pings.topsitesImpression.submit() + } + } + @Test fun handleStartBrowsingClicked() { var hideOnboardingInvoked = false diff --git a/app/src/test/java/org/mozilla/fenix/home/topsites/TopSiteItemViewHolderTest.kt b/app/src/test/java/org/mozilla/fenix/home/topsites/TopSiteItemViewHolderTest.kt index b6be453d7..d00d93156 100644 --- a/app/src/test/java/org/mozilla/fenix/home/topsites/TopSiteItemViewHolderTest.kt +++ b/app/src/test/java/org/mozilla/fenix/home/topsites/TopSiteItemViewHolderTest.kt @@ -17,6 +17,10 @@ import org.junit.Assert.assertNull import org.junit.Before import org.junit.Test import org.junit.runner.RunWith +import org.mozilla.fenix.GleanMetrics.Pings +import org.mozilla.fenix.GleanMetrics.TopSites +import org.mozilla.fenix.components.metrics.Event +import org.mozilla.fenix.components.metrics.MetricController import org.mozilla.fenix.databinding.TopSiteItemBinding import org.mozilla.fenix.ext.components import org.mozilla.fenix.helpers.FenixRobolectricTestRunner @@ -28,6 +32,7 @@ class TopSiteItemViewHolderTest { private lateinit var binding: TopSiteItemBinding private lateinit var interactor: TopSiteInteractor private lateinit var lifecycleOwner: LifecycleOwner + private lateinit var metrics: MetricController private val pocket = TopSite.Default( id = 1L, @@ -41,22 +46,24 @@ class TopSiteItemViewHolderTest { binding = TopSiteItemBinding.inflate(LayoutInflater.from(testContext)) interactor = mockk(relaxed = true) lifecycleOwner = mockk(relaxed = true) + metrics = mockk(relaxed = true) every { testContext.components.core.icons } returns BrowserIcons(testContext, mockk(relaxed = true)) + every { testContext.components.analytics.metrics } returns metrics } @Test fun `calls interactor on click`() { - TopSiteItemViewHolder(binding.root, lifecycleOwner, interactor).bind(pocket) + TopSiteItemViewHolder(binding.root, lifecycleOwner, interactor).bind(pocket, position = 0) binding.topSiteItem.performClick() - verify { interactor.onSelectTopSite(pocket) } + verify { interactor.onSelectTopSite(pocket, position = 0) } } @Test fun `calls interactor on long click`() { every { testContext.components.analytics } returns mockk(relaxed = true) - TopSiteItemViewHolder(binding.root, lifecycleOwner, interactor).bind(pocket) + TopSiteItemViewHolder(binding.root, lifecycleOwner, interactor).bind(pocket, position = 0) binding.topSiteItem.performLongClick() verify { interactor.onTopSiteMenuOpened() } @@ -71,7 +78,7 @@ class TopSiteItemViewHolderTest { createdAt = 0 ) - TopSiteItemViewHolder(binding.root, lifecycleOwner, interactor).bind(defaultTopSite) + TopSiteItemViewHolder(binding.root, lifecycleOwner, interactor).bind(defaultTopSite, position = 0) val pinIndicator = binding.topSiteTitle.compoundDrawables[0] assertNotNull(pinIndicator) @@ -86,7 +93,7 @@ class TopSiteItemViewHolderTest { createdAt = 0 ) - TopSiteItemViewHolder(binding.root, lifecycleOwner, interactor).bind(pinnedTopSite) + TopSiteItemViewHolder(binding.root, lifecycleOwner, interactor).bind(pinnedTopSite, position = 0) val pinIndicator = binding.topSiteTitle.compoundDrawables[0] assertNotNull(pinIndicator) @@ -101,9 +108,39 @@ class TopSiteItemViewHolderTest { createdAt = 0 ) - TopSiteItemViewHolder(binding.root, lifecycleOwner, interactor).bind(frecentTopSite) + TopSiteItemViewHolder(binding.root, lifecycleOwner, interactor).bind(frecentTopSite, position = 0) val pinIndicator = binding.topSiteTitle.compoundDrawables[0] assertNull(pinIndicator) } + + @Test + fun `GIVEN a provided top site and position WHEN the provided top site is shown THEN submit a top site impression ping`() { + 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://example.com", + createdAt = 3 + ) + val position = 0 + + TopSiteItemViewHolder(binding.root, lifecycleOwner, interactor).submitTopSitesImpressionPing(topSite, position) + + verify { + metrics.track( + Event.TopSiteContileImpression( + position = position + 1, + source = Event.TopSiteContileImpression.Source.NEWTAB + ) + ) + + TopSites.contileTileId.set(3) + TopSites.contileAdvertiser.set("mozilla") + TopSites.contileReportingUrl.set(topSite.impressionUrl) + Pings.topsitesImpression.submit() + } + } }