From 87bde8192c8e822b974f60e1f24f7b8bb357c1f8 Mon Sep 17 00:00:00 2001 From: DreVla Date: Tue, 17 Oct 2023 16:44:09 +0300 Subject: [PATCH] Bug 1854501 - Add telemetry to count Fakespot exposures Added a new probe `product_page_visits` which counts the number of visits to a supported retailer product page. (cherry picked from commit fbd860cb37334374806b835ad548b82e050c820a) --- app/metrics.yaml | 20 ++ .../mozilla/fenix/browser/BrowserFragment.kt | 5 +- .../shopping/ReviewQualityCheckFeature.kt | 22 ++- .../shopping/ReviewQualityCheckFeatureTest.kt | 186 ++++++++++++++++-- 4 files changed, 210 insertions(+), 23 deletions(-) diff --git a/app/metrics.yaml b/app/metrics.yaml index 68d83a17a1..0c41fa17d2 100644 --- a/app/metrics.yaml +++ b/app/metrics.yaml @@ -10773,6 +10773,26 @@ shopping: metadata: tags: - Shopping + product_page_visits: + type: counter + description: | + Counts number of visits to a supported retailer product page + while enrolled in either the control or treatment branches + of the shopping experiment. + send_in_pings: + - metrics + bugs: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1854501 + data_reviews: + - https://github.com/mozilla-mobile/firefox-android/pull/4120#issuecomment-1768423370 + data_sensitivity: + - interaction + notification_emails: + - android-probes@mozilla.com + expires: never + metadata: + tags: + - Shopping shopping.settings: component_opted_out: diff --git a/app/src/main/java/org/mozilla/fenix/browser/BrowserFragment.kt b/app/src/main/java/org/mozilla/fenix/browser/BrowserFragment.kt index 65364ee530..d32fa7105b 100644 --- a/app/src/main/java/org/mozilla/fenix/browser/BrowserFragment.kt +++ b/app/src/main/java/org/mozilla/fenix/browser/BrowserFragment.kt @@ -288,7 +288,7 @@ class BrowserFragment : BaseBrowserFragment(), UserInteractionHandler { shoppingExperienceFeature = DefaultShoppingExperienceFeature( settings = requireContext().settings(), ), - onAvailabilityChange = { + onIconVisibilityChange = { if (!reviewQualityCheckAvailable && it) { Shopping.addressBarIconDisplayed.record() } @@ -298,6 +298,9 @@ class BrowserFragment : BaseBrowserFragment(), UserInteractionHandler { onBottomSheetStateChange = { reviewQualityCheck.setSelected(selected = it, notifyListener = false) }, + onProductPageDetected = { + Shopping.productPageVisits.add() + }, ), owner = this, view = view, diff --git a/app/src/main/java/org/mozilla/fenix/shopping/ReviewQualityCheckFeature.kt b/app/src/main/java/org/mozilla/fenix/shopping/ReviewQualityCheckFeature.kt index 37625903da..68363b7626 100644 --- a/app/src/main/java/org/mozilla/fenix/shopping/ReviewQualityCheckFeature.kt +++ b/app/src/main/java/org/mozilla/fenix/shopping/ReviewQualityCheckFeature.kt @@ -27,37 +27,41 @@ private const val DEBOUNCE_TIMEOUT_MILLIS = 200L * @property appStore Reference to the application's [AppStore]. * @property browserStore Reference to the application's [BrowserStore]. * @property shoppingExperienceFeature Reference to the [ShoppingExperienceFeature]. - * @property onAvailabilityChange Invoked when availability of this feature changes based on feature + * @property onIconVisibilityChange Invoked when shopping icon visibility changes based on feature * flag and when the loaded page is a supported product page. * @property onBottomSheetStateChange Invoked when the bottom sheet is collapsed or expanded. * @property debounceTimeoutMillis Function that returns the debounce timeout in milliseconds. This * make it possible to wait till [ContentState.isProductUrl] is stable before invoking - * [onAvailabilityChange]. + * [onIconVisibilityChange]. + * @property onProductPageDetected Invoked when a product page is detected and loaded. Used to + * detect when to send telemetry for shopping.product_page_visits. */ @OptIn(FlowPreview::class) class ReviewQualityCheckFeature( private val appStore: AppStore, private val browserStore: BrowserStore, private val shoppingExperienceFeature: ShoppingExperienceFeature, - private val onAvailabilityChange: (isAvailable: Boolean) -> Unit, + private val onIconVisibilityChange: (isAvailable: Boolean) -> Unit, private val onBottomSheetStateChange: (isExpanded: Boolean) -> Unit, private val debounceTimeoutMillis: (Boolean) -> Long = { if (it) DEBOUNCE_TIMEOUT_MILLIS else 0 }, + private val onProductPageDetected: () -> Unit, ) : LifecycleAwareFeature { private var scope: CoroutineScope? = null private var appStoreScope: CoroutineScope? = null override fun start() { - if (!shoppingExperienceFeature.isEnabled) { - onAvailabilityChange(false) - return - } - scope = browserStore.flowScoped { flow -> flow.mapNotNull { it.selectedTab } .map { it.content.isProductUrl && !it.content.loading } .distinctUntilChanged() .debounce(debounceTimeoutMillis) - .collect(onAvailabilityChange) + .collect { + if (it) { + onProductPageDetected() + } + + onIconVisibilityChange(shoppingExperienceFeature.isEnabled && it) + } } appStoreScope = appStore.flowScoped { flow -> diff --git a/app/src/test/java/org/mozilla/fenix/shopping/ReviewQualityCheckFeatureTest.kt b/app/src/test/java/org/mozilla/fenix/shopping/ReviewQualityCheckFeatureTest.kt index 5053f8edc4..de34b236b2 100644 --- a/app/src/test/java/org/mozilla/fenix/shopping/ReviewQualityCheckFeatureTest.kt +++ b/app/src/test/java/org/mozilla/fenix/shopping/ReviewQualityCheckFeatureTest.kt @@ -30,20 +30,34 @@ class ReviewQualityCheckFeatureTest { val coroutinesTestRule = MainCoroutineRule() @Test - fun `WHEN feature is not enabled THEN callback returns false`() { + fun `WHEN feature is not enabled THEN callback returns false`() = runTest { var availability: Boolean? = null + val tab = createTab( + url = "https://www.mozilla.org", + id = "test-tab", + isProductUrl = true, + ) + val browserState = BrowserState( + tabs = listOf(tab), + selectedTabId = tab.id, + ) val tested = ReviewQualityCheckFeature( appStore = AppStore(), - browserStore = BrowserStore(), + browserStore = BrowserStore( + initialState = browserState, + ), shoppingExperienceFeature = FakeShoppingExperienceFeature(enabled = false), - onAvailabilityChange = { + onIconVisibilityChange = { availability = it }, onBottomSheetStateChange = {}, + onProductPageDetected = {}, ) tested.start() + testScheduler.advanceTimeBy(250) + assertFalse(availability!!) } @@ -66,10 +80,11 @@ class ReviewQualityCheckFeatureTest { initialState = browserState, ), shoppingExperienceFeature = FakeShoppingExperienceFeature(), - onAvailabilityChange = { + onIconVisibilityChange = { availability = it }, onBottomSheetStateChange = {}, + onProductPageDetected = {}, ) tested.start() @@ -99,10 +114,11 @@ class ReviewQualityCheckFeatureTest { initialState = browserState, ), shoppingExperienceFeature = FakeShoppingExperienceFeature(), - onAvailabilityChange = { + onIconVisibilityChange = { availability = it }, onBottomSheetStateChange = {}, + onProductPageDetected = {}, ) tested.start() @@ -129,11 +145,12 @@ class ReviewQualityCheckFeatureTest { initialState = browserState, ), shoppingExperienceFeature = FakeShoppingExperienceFeature(), - onAvailabilityChange = { + onIconVisibilityChange = { availability = it }, onBottomSheetStateChange = {}, debounceTimeoutMillis = { 0 }, + onProductPageDetected = {}, ) tested.start() @@ -165,11 +182,12 @@ class ReviewQualityCheckFeatureTest { appStore = AppStore(), browserStore = browserStore, shoppingExperienceFeature = FakeShoppingExperienceFeature(), - onAvailabilityChange = { + onIconVisibilityChange = { availability = it }, onBottomSheetStateChange = {}, debounceTimeoutMillis = { 0 }, + onProductPageDetected = {}, ) tested.start() @@ -204,11 +222,12 @@ class ReviewQualityCheckFeatureTest { appStore = AppStore(), browserStore = browserStore, shoppingExperienceFeature = FakeShoppingExperienceFeature(), - onAvailabilityChange = { + onIconVisibilityChange = { availability = it }, onBottomSheetStateChange = {}, debounceTimeoutMillis = { 0 }, + onProductPageDetected = {}, ) tested.start() @@ -238,10 +257,11 @@ class ReviewQualityCheckFeatureTest { appStore = AppStore(), browserStore = browserStore, shoppingExperienceFeature = FakeShoppingExperienceFeature(), - onAvailabilityChange = { + onIconVisibilityChange = { availability.add(it) }, onBottomSheetStateChange = {}, + onProductPageDetected = {}, ) tested.start() @@ -301,11 +321,12 @@ class ReviewQualityCheckFeatureTest { appStore = AppStore(), browserStore = browserStore, shoppingExperienceFeature = FakeShoppingExperienceFeature(), - onAvailabilityChange = { + onIconVisibilityChange = { availability = it availabilityCount++ }, onBottomSheetStateChange = {}, + onProductPageDetected = {}, ) tested.start() @@ -329,10 +350,11 @@ class ReviewQualityCheckFeatureTest { appStore = appStore, browserStore = BrowserStore(), shoppingExperienceFeature = FakeShoppingExperienceFeature(), - onAvailabilityChange = {}, + onIconVisibilityChange = {}, onBottomSheetStateChange = { isExpanded = it }, + onProductPageDetected = {}, ) tested.start() @@ -354,10 +376,11 @@ class ReviewQualityCheckFeatureTest { appStore = appStore, browserStore = BrowserStore(), shoppingExperienceFeature = FakeShoppingExperienceFeature(), - onAvailabilityChange = {}, + onIconVisibilityChange = {}, onBottomSheetStateChange = { isExpanded = it }, + onProductPageDetected = {}, ) tested.start() @@ -379,10 +402,11 @@ class ReviewQualityCheckFeatureTest { appStore = appStore, browserStore = BrowserStore(), shoppingExperienceFeature = FakeShoppingExperienceFeature(), - onAvailabilityChange = {}, + onIconVisibilityChange = {}, onBottomSheetStateChange = { isExpanded = it }, + onProductPageDetected = {}, ) tested.start() @@ -394,4 +418,140 @@ class ReviewQualityCheckFeatureTest { tested.start() assertFalse(isExpanded!!) } + + @Test + fun `GIVEN feature is enabled WHEN non product url accessed THEN callback not called`() { + runTest { + var invokedCounter = 0 + val tab = createTab( + url = "https://www.mozilla.org", + id = "test-tab", + isProductUrl = false, + ) + val browserState = BrowserState( + tabs = listOf(tab), + selectedTabId = tab.id, + ) + val tested = ReviewQualityCheckFeature( + appStore = AppStore(), + browserStore = BrowserStore( + initialState = browserState, + ), + shoppingExperienceFeature = FakeShoppingExperienceFeature(), + onIconVisibilityChange = {}, + onBottomSheetStateChange = {}, + debounceTimeoutMillis = { 0 }, + onProductPageDetected = { + invokedCounter++ + }, + ) + + tested.start() + + assertEquals(invokedCounter, 0) + } + } + + @Test + fun `GIVEN feature is enabled WHEN product url accessed THEN callback called`() { + runTest { + var invokedCounter = 0 + val tab = createTab( + url = "https://www.shopping.org", + id = "test-tab", + isProductUrl = true, + ).let { + it.copy(content = it.content.copy(loading = false)) + } + val browserState = BrowserState( + tabs = listOf(tab), + selectedTabId = tab.id, + ) + val tested = ReviewQualityCheckFeature( + appStore = AppStore(), + browserStore = BrowserStore( + initialState = browserState, + ), + shoppingExperienceFeature = FakeShoppingExperienceFeature(), + onIconVisibilityChange = {}, + onBottomSheetStateChange = {}, + debounceTimeoutMillis = { 0 }, + onProductPageDetected = { + invokedCounter++ + }, + ) + + tested.start() + + assertEquals(invokedCounter, 1) + } + } + + @Test + fun `GIVEN feature is disabled WHEN non product url accessed THEN callback not called`() { + runTest { + var invokedCounter = 0 + val tab = createTab( + url = "https://www.mozilla.org", + id = "test-tab", + isProductUrl = false, + ) + val browserState = BrowserState( + tabs = listOf(tab), + selectedTabId = tab.id, + ) + val tested = ReviewQualityCheckFeature( + appStore = AppStore(), + browserStore = BrowserStore( + initialState = browserState, + ), + shoppingExperienceFeature = FakeShoppingExperienceFeature(enabled = false), + onIconVisibilityChange = {}, + onBottomSheetStateChange = {}, + debounceTimeoutMillis = { 0 }, + onProductPageDetected = { + invokedCounter++ + }, + ) + + tested.start() + + assertEquals(invokedCounter, 0) + } + } + + @Test + fun `GIVEN feature is disabled WHEN product url accessed THEN callback called`() { + runTest { + var invokedCounter = 0 + val tab = createTab( + url = "https://www.mozilla.org", + id = "test-tab", + isProductUrl = true, + ).let { + it.copy(content = it.content.copy(loading = false)) + } + val browserState = BrowserState( + tabs = listOf(tab), + selectedTabId = tab.id, + ) + val tested = ReviewQualityCheckFeature( + appStore = AppStore(), + browserStore = BrowserStore( + initialState = browserState, + ), + shoppingExperienceFeature = FakeShoppingExperienceFeature(enabled = false), + onIconVisibilityChange = {}, + onBottomSheetStateChange = {}, + debounceTimeoutMillis = { 0 }, + onProductPageDetected = { + invokedCounter++ + }, + ) + + tested.start() + + assertEquals(invokedCounter, 1) + } + } }