diff --git a/app/metrics.yaml b/app/metrics.yaml index c16b223aa7e3..947c89bd6fd1 100644 --- a/app/metrics.yaml +++ b/app/metrics.yaml @@ -5890,6 +5890,28 @@ pocket: metadata: tags: - PocketIntegration + spoc_shim: + type: text + description: | + Shim data of the Pocket sponsored story the user just + interacted with. + The shim is a unique base64 string identifying each story and + type of user interaction: story impression or click. + bugs: + - https://github.com/mozilla-mobile/fenix/issues/27549 + - https://mozilla-hub.atlassian.net/browse/FNXV2-21791 + data_reviews: + - https://github.com/mozilla-mobile/fenix/pull/27550#issuecomment-1295027631 + data_sensitivity: + - web_activity + notification_emails: + - android-probes@mozilla.com + expires: never + send_in_pings: + - spoc + metadata: + tags: + - PocketIntegration home_recs_spoc_shown: type: event description: | diff --git a/app/pings.yaml b/app/pings.yaml index ef205112c9a6..981168f5b4a5 100644 --- a/app/pings.yaml +++ b/app/pings.yaml @@ -43,3 +43,22 @@ topsites-impression: - https://github.com/mozilla-mobile/fenix/pull/23945 notification_emails: - android-probes@mozilla.com + +spoc: + description: | + Contains data identifying with which Pocket sponsored story the user + interacted with and the type of interaction: story impression or click. + include_client_id: false + reasons: + impression: | + A sponsored story had more than 50% of it's content visible + on the screen. + click: | + A sponsored story was clicked by the user. + bugs: + - https://github.com/mozilla-mobile/fenix/issues/27549 + - https://mozilla-hub.atlassian.net/browse/FNXV2-21791 + data_reviews: + - https://github.com/mozilla-mobile/fenix/pull/27550#issuecomment-1295027631 + notification_emails: + - android-probes@mozilla.com diff --git a/app/src/main/java/org/mozilla/fenix/home/pocket/PocketStoriesController.kt b/app/src/main/java/org/mozilla/fenix/home/pocket/PocketStoriesController.kt index 0bb1618fd3f5..0a660e56e8b9 100644 --- a/app/src/main/java/org/mozilla/fenix/home/pocket/PocketStoriesController.kt +++ b/app/src/main/java/org/mozilla/fenix/home/pocket/PocketStoriesController.kt @@ -12,6 +12,7 @@ import mozilla.components.service.pocket.PocketStory.PocketRecommendedStory import mozilla.components.service.pocket.PocketStory.PocketSponsoredStory import mozilla.components.service.pocket.ext.getCurrentFlightImpressions import org.mozilla.fenix.BrowserDirection +import org.mozilla.fenix.GleanMetrics.Pings import org.mozilla.fenix.GleanMetrics.Pocket import org.mozilla.fenix.HomeActivity import org.mozilla.fenix.R @@ -93,6 +94,8 @@ internal class DefaultPocketStoriesController( timesShown = storyShown.getCurrentFlightImpressions().size.inc().toString(), ), ) + Pocket.spocShim.set(storyShown.shim.impression) + Pings.spoc.submit(Pings.spocReasonCodes.impression) } else -> { // no-op @@ -169,6 +172,8 @@ internal class DefaultPocketStoriesController( timesShown = storyClicked.getCurrentFlightImpressions().size.inc().toString(), ), ) + Pocket.spocShim.set(storyClicked.shim.click) + Pings.spoc.submit(Pings.spocReasonCodes.click) } } } diff --git a/app/src/test/java/org/mozilla/fenix/home/pocket/DefaultPocketStoriesControllerTest.kt b/app/src/test/java/org/mozilla/fenix/home/pocket/DefaultPocketStoriesControllerTest.kt index fd6acd75dd5c..38c5f2ee21ba 100644 --- a/app/src/test/java/org/mozilla/fenix/home/pocket/DefaultPocketStoriesControllerTest.kt +++ b/app/src/test/java/org/mozilla/fenix/home/pocket/DefaultPocketStoriesControllerTest.kt @@ -7,12 +7,14 @@ package org.mozilla.fenix.home.pocket import androidx.navigation.NavController import io.mockk.every import io.mockk.mockk +import io.mockk.mockkStatic import io.mockk.spyk import io.mockk.verify import io.mockk.verifyOrder import mozilla.components.service.pocket.PocketStory import mozilla.components.service.pocket.PocketStory.PocketRecommendedStory import mozilla.components.service.pocket.PocketStory.PocketSponsoredStory +import mozilla.components.service.pocket.ext.getCurrentFlightImpressions import mozilla.components.support.test.robolectric.testContext import mozilla.telemetry.glean.testing.GleanTestRule import org.junit.Assert.assertEquals @@ -23,6 +25,7 @@ import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith import org.mozilla.fenix.BrowserDirection +import org.mozilla.fenix.GleanMetrics.Pings import org.mozilla.fenix.GleanMetrics.Pocket import org.mozilla.fenix.HomeActivity import org.mozilla.fenix.R @@ -173,13 +176,31 @@ class DefaultPocketStoriesControllerTest { fun `WHEN a new sponsored story is shown THEN update the State and record telemetry`() { val store = spyk(AppStore()) val controller = DefaultPocketStoriesController(mockk(), store, mockk()) - val storyShown: PocketSponsoredStory = mockk(relaxed = true) - val storyGridLocation = 1 to 2 - - controller.handleStoryShown(storyShown, storyGridLocation) - - verify { store.dispatch(AppAction.PocketStoriesShown(listOf(storyShown))) } - assertNotNull(Pocket.homeRecsSpocShown.testGetValue()) + val storyShown: PocketSponsoredStory = mockk { + every { shim.click } returns "testClickShim" + every { shim.impression } returns "testImpressionShim" + } + var wasPingSent = false + mockkStatic("mozilla.components.service.pocket.ext.PocketStoryKt") { + // Simulate that the story was already shown 3 times. + every { storyShown.getCurrentFlightImpressions() } returns listOf(2L, 3L, 7L) + // Test that the spoc ping is immediately sent with the needed data. + Pings.spoc.testBeforeNextSubmit { reason -> + assertEquals(storyShown.shim.impression, Pocket.spocShim.testGetValue()) + assertEquals(Pings.spocReasonCodes.impression.name, reason?.name) + wasPingSent = true + } + + controller.handleStoryShown(storyShown, 1 to 2) + + verify { store.dispatch(AppAction.PocketStoriesShown(listOf(storyShown))) } + assertNotNull(Pocket.homeRecsSpocShown.testGetValue()) + assertEquals(1, Pocket.homeRecsSpocShown.testGetValue()!!.size) + val data = Pocket.homeRecsSpocShown.testGetValue()!!.single().extra + assertEquals("1x2", data?.entries?.first { it.key == "position" }?.value) + assertEquals("4", data?.entries?.first { it.key == "times_shown" }?.value) + assertTrue(wasPingSent) + } } @Test @@ -227,24 +248,43 @@ class DefaultPocketStoriesControllerTest { @Test fun `WHEN a sponsored story is clicked THEN open that story's url using HomeActivity and record telemetry`() { - val story = PocketSponsoredStory( + val storyClicked = PocketSponsoredStory( id = 7, title = "", url = "testLink", imageUrl = "", sponsor = "", - shim = mockk(), + shim = mockk { + every { click } returns "testClickShim" + every { impression } returns "testImpressionShim" + }, priority = 3, caps = mockk(relaxed = true), ) val homeActivity: HomeActivity = mockk(relaxed = true) val controller = DefaultPocketStoriesController(homeActivity, mockk(), mockk(relaxed = true)) + var wasPingSent = false assertNull(Pocket.homeRecsSpocClicked.testGetValue()) - - controller.handleStoryClicked(story, 1 to 2) - - verify { homeActivity.openToBrowserAndLoad(story.url, true, BrowserDirection.FromHome) } - assertNull(Pocket.homeRecsStoryClicked.testGetValue()) + mockkStatic("mozilla.components.service.pocket.ext.PocketStoryKt") { + // Simulate that the story was already shown 2 times. + every { storyClicked.getCurrentFlightImpressions() } returns listOf(2L, 3L) + // Test that the spoc ping is immediately sent with the needed data. + Pings.spoc.testBeforeNextSubmit { reason -> + assertEquals(storyClicked.shim.click, Pocket.spocShim.testGetValue()) + assertEquals(Pings.spocReasonCodes.click.name, reason?.name) + wasPingSent = true + } + + controller.handleStoryClicked(storyClicked, 2 to 3) + + verify { homeActivity.openToBrowserAndLoad(storyClicked.url, true, BrowserDirection.FromHome) } + assertNotNull(Pocket.homeRecsSpocClicked.testGetValue()) + assertEquals(1, Pocket.homeRecsSpocClicked.testGetValue()!!.size) + val data = Pocket.homeRecsSpocClicked.testGetValue()!!.single().extra + assertEquals("2x3", data?.entries?.first { it.key == "position" }?.value) + assertEquals("3", data?.entries?.first { it.key == "times_shown" }?.value) + assertTrue(wasPingSent) + } } @Test