From c4d76dce5aae263f871a67276d2851286ae92dc6 Mon Sep 17 00:00:00 2001 From: Mihai Branescu Date: Wed, 22 Apr 2020 11:46:40 +0300 Subject: [PATCH] For #6558 - cleanup + added unit tests --- app/metrics.yaml | 6 +- app/src/main/assets/extensions/ads/ads.js | 39 +++++- .../mozilla/fenix/ads/SearchProviderCookie.kt | 10 -- .../fenix/browser/TelemetrySessionObserver.kt | 6 +- .../fenix/browser/UriOpenedObserver.kt | 2 +- .../java/org/mozilla/fenix/components/Core.kt | 2 +- .../main/java/org/mozilla/fenix/ext/Ads.kt | 6 +- .../java/org/mozilla/fenix/ext/JsonArray.kt | 12 -- .../search/telemetry/SearchProviderCookie.kt | 14 ++ .../telemetry}/SearchProviderModel.kt | 6 +- .../telemetry}/ads/AdsTelemetry.kt | 37 +++-- .../browser/TelemetrySessionObserverTest.kt | 120 ++++++++++++++++ .../fenix/browser/UriOpenedObserverTest.kt | 34 +---- .../java/org/mozilla/fenix/ext/AdsTest.kt | 41 ++++++ .../search/telemetry/ads/AdsTelemetryTest.kt | 128 ++++++++++++++++++ docs/metrics.md | 6 +- 16 files changed, 393 insertions(+), 76 deletions(-) delete mode 100644 app/src/main/java/org/mozilla/fenix/ads/SearchProviderCookie.kt delete mode 100644 app/src/main/java/org/mozilla/fenix/ext/JsonArray.kt create mode 100644 app/src/main/java/org/mozilla/fenix/search/telemetry/SearchProviderCookie.kt rename app/src/main/java/org/mozilla/fenix/{ads => search/telemetry}/SearchProviderModel.kt (59%) rename app/src/main/java/org/mozilla/fenix/{ => search/telemetry}/ads/AdsTelemetry.kt (81%) create mode 100644 app/src/test/java/org/mozilla/fenix/browser/TelemetrySessionObserverTest.kt create mode 100644 app/src/test/java/org/mozilla/fenix/ext/AdsTest.kt create mode 100644 app/src/test/java/org/mozilla/fenix/search/telemetry/ads/AdsTelemetryTest.kt diff --git a/app/metrics.yaml b/app/metrics.yaml index 66fb1678e..d52317419 100644 --- a/app/metrics.yaml +++ b/app/metrics.yaml @@ -2007,11 +2007,12 @@ browser.search: description: > Records counts of SERP pages with adverts displayed. The key format is ‘’. send_in_pings: + - baseline - metrics bugs: - https://github.com/mozilla-mobile/fenix/issues/6558 data_reviews: - - https://github.com/mozilla-mobile/fenix/issues/6558 + - https://github.com/mozilla-mobile/fenix/pull/10112 notification_emails: - fenix-core@mozilla.com expires: "2020-09-01" @@ -2020,11 +2021,12 @@ browser.search: description: > Records clicks of adverts on SERP pages. The key format is ‘’. send_in_pings: + - baseline - metrics bugs: - https://github.com/mozilla-mobile/fenix/issues/6558 data_reviews: - - https://github.com/mozilla-mobile/fenix/issues/6558 + - https://github.com/mozilla-mobile/fenix/pull/10112 notification_emails: - fenix-core@mozilla.com expires: "2020-09-01" diff --git a/app/src/main/assets/extensions/ads/ads.js b/app/src/main/assets/extensions/ads/ads.js index 28b6f13f7..911fbe48a 100644 --- a/app/src/main/assets/extensions/ads/ads.js +++ b/app/src/main/assets/extensions/ads/ads.js @@ -1,4 +1,10 @@ -function collect_urls(urls) { +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + + const ADLINK_CHECK_TIMEOUT_MS = 1000; + +function collectLinks(urls) { let anchors = document.getElementsByTagName("a"); for (let anchor of anchors) { if (!anchor.href) { @@ -7,11 +13,30 @@ function collect_urls(urls) { urls.push(anchor.href); } } -let urls = []; -collect_urls(urls) -let message = { - 'url': document.location.href, - 'urls': urls +function sendLinks() { + let urls = []; + collectLinks(urls); + + let message = { + 'url': document.location.href, + 'urls': urls + }; + browser.runtime.sendNativeMessage("MozacBrowserAds", message); } -browser.runtime.sendNativeMessage("MozacBrowserAds", message); + +var timeout; + +window.onload = function() { + timeout = setTimeout(sendLinks, ADLINK_CHECK_TIMEOUT_MS); +}; + +window.onpageshow = function(event) { + if (event.persisted) { + timeout = setTimeout(sendLinks, ADLINK_CHECK_TIMEOUT_MS); + } +}; + +window.onunload = function() { + clearTimeout(timeout); +}; diff --git a/app/src/main/java/org/mozilla/fenix/ads/SearchProviderCookie.kt b/app/src/main/java/org/mozilla/fenix/ads/SearchProviderCookie.kt deleted file mode 100644 index 47615af9c..000000000 --- a/app/src/main/java/org/mozilla/fenix/ads/SearchProviderCookie.kt +++ /dev/null @@ -1,10 +0,0 @@ -package org.mozilla.fenix.ads - -data class SearchProviderCookie( - val extraCodeParam: String, - val extraCodePrefixes: List, - val host: String, - val name: String, - val codeParam: String, - val codePrefixes: List -) diff --git a/app/src/main/java/org/mozilla/fenix/browser/TelemetrySessionObserver.kt b/app/src/main/java/org/mozilla/fenix/browser/TelemetrySessionObserver.kt index 912b711ba..46733f079 100644 --- a/app/src/main/java/org/mozilla/fenix/browser/TelemetrySessionObserver.kt +++ b/app/src/main/java/org/mozilla/fenix/browser/TelemetrySessionObserver.kt @@ -1,10 +1,14 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + package org.mozilla.fenix.browser import androidx.annotation.VisibleForTesting import mozilla.components.browser.session.Session -import org.mozilla.fenix.ads.AdsTelemetry import org.mozilla.fenix.components.metrics.Event import org.mozilla.fenix.components.metrics.MetricController +import org.mozilla.fenix.search.telemetry.ads.AdsTelemetry class TelemetrySessionObserver( private val metrics: MetricController, diff --git a/app/src/main/java/org/mozilla/fenix/browser/UriOpenedObserver.kt b/app/src/main/java/org/mozilla/fenix/browser/UriOpenedObserver.kt index 8b8d8e815..35eaf7e35 100644 --- a/app/src/main/java/org/mozilla/fenix/browser/UriOpenedObserver.kt +++ b/app/src/main/java/org/mozilla/fenix/browser/UriOpenedObserver.kt @@ -9,7 +9,7 @@ import androidx.fragment.app.FragmentActivity import androidx.lifecycle.LifecycleOwner import mozilla.components.browser.session.Session import mozilla.components.browser.session.SessionManager -import org.mozilla.fenix.ads.AdsTelemetry +import org.mozilla.fenix.search.telemetry.ads.AdsTelemetry import org.mozilla.fenix.components.metrics.MetricController import org.mozilla.fenix.ext.components import org.mozilla.fenix.ext.metrics diff --git a/app/src/main/java/org/mozilla/fenix/components/Core.kt b/app/src/main/java/org/mozilla/fenix/components/Core.kt index 13ff02d1c..20e03b93b 100644 --- a/app/src/main/java/org/mozilla/fenix/components/Core.kt +++ b/app/src/main/java/org/mozilla/fenix/components/Core.kt @@ -42,7 +42,7 @@ import org.mozilla.fenix.AppRequestInterceptor import org.mozilla.fenix.Config import org.mozilla.fenix.HomeActivity import org.mozilla.fenix.R -import org.mozilla.fenix.ads.AdsTelemetry +import org.mozilla.fenix.search.telemetry.ads.AdsTelemetry import org.mozilla.fenix.ext.components import org.mozilla.fenix.ext.settings import org.mozilla.fenix.media.MediaService diff --git a/app/src/main/java/org/mozilla/fenix/ext/Ads.kt b/app/src/main/java/org/mozilla/fenix/ext/Ads.kt index d27e25e79..7bf818598 100644 --- a/app/src/main/java/org/mozilla/fenix/ext/Ads.kt +++ b/app/src/main/java/org/mozilla/fenix/ext/Ads.kt @@ -1,6 +1,10 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + package org.mozilla.fenix.ext -import org.mozilla.fenix.ads.SearchProviderModel +import org.mozilla.fenix.search.telemetry.SearchProviderModel fun SearchProviderModel.containsAds(urlList: List): Boolean { return urlList.containsAds(this.extraAdServersRegexps) diff --git a/app/src/main/java/org/mozilla/fenix/ext/JsonArray.kt b/app/src/main/java/org/mozilla/fenix/ext/JsonArray.kt deleted file mode 100644 index ab49469e6..000000000 --- a/app/src/main/java/org/mozilla/fenix/ext/JsonArray.kt +++ /dev/null @@ -1,12 +0,0 @@ -package org.mozilla.fenix.ext - -import org.json.JSONArray - -@Suppress("UNCHECKED_CAST") -fun JSONArray.toList(): List { - val result = ArrayList() - for (i in 0 until length()) { - result.add(get(i) as T) - } - return result -} diff --git a/app/src/main/java/org/mozilla/fenix/search/telemetry/SearchProviderCookie.kt b/app/src/main/java/org/mozilla/fenix/search/telemetry/SearchProviderCookie.kt new file mode 100644 index 000000000..5e4372e9e --- /dev/null +++ b/app/src/main/java/org/mozilla/fenix/search/telemetry/SearchProviderCookie.kt @@ -0,0 +1,14 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.fenix.search.telemetry + +data class SearchProviderCookie( + val extraCodeParam: String, + val extraCodePrefixes: List, + val host: String, + val name: String, + val codeParam: String, + val codePrefixes: List +) diff --git a/app/src/main/java/org/mozilla/fenix/ads/SearchProviderModel.kt b/app/src/main/java/org/mozilla/fenix/search/telemetry/SearchProviderModel.kt similarity index 59% rename from app/src/main/java/org/mozilla/fenix/ads/SearchProviderModel.kt rename to app/src/main/java/org/mozilla/fenix/search/telemetry/SearchProviderModel.kt index e105e014a..a1894c98e 100644 --- a/app/src/main/java/org/mozilla/fenix/ads/SearchProviderModel.kt +++ b/app/src/main/java/org/mozilla/fenix/search/telemetry/SearchProviderModel.kt @@ -1,4 +1,8 @@ -package org.mozilla.fenix.ads +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.fenix.search.telemetry data class SearchProviderModel( val name: String, diff --git a/app/src/main/java/org/mozilla/fenix/ads/AdsTelemetry.kt b/app/src/main/java/org/mozilla/fenix/search/telemetry/ads/AdsTelemetry.kt similarity index 81% rename from app/src/main/java/org/mozilla/fenix/ads/AdsTelemetry.kt rename to app/src/main/java/org/mozilla/fenix/search/telemetry/ads/AdsTelemetry.kt index 3db3d9ff4..27bef0bbe 100644 --- a/app/src/main/java/org/mozilla/fenix/ads/AdsTelemetry.kt +++ b/app/src/main/java/org/mozilla/fenix/search/telemetry/ads/AdsTelemetry.kt @@ -1,5 +1,10 @@ -package org.mozilla.fenix.ads +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +package org.mozilla.fenix.search.telemetry.ads + +import androidx.annotation.VisibleForTesting import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.collect import kotlinx.coroutines.flow.map @@ -11,16 +16,19 @@ import mozilla.components.concept.engine.webextension.MessageHandler import mozilla.components.concept.engine.webextension.WebExtension import mozilla.components.lib.state.ext.flowScoped import mozilla.components.support.base.log.logger.Logger +import mozilla.components.support.ktx.android.org.json.toList import mozilla.components.support.ktx.kotlinx.coroutines.flow.filterChanged import org.json.JSONObject import org.mozilla.fenix.components.metrics.Event import org.mozilla.fenix.components.metrics.MetricController import org.mozilla.fenix.ext.containsAds -import org.mozilla.fenix.ext.toList +import org.mozilla.fenix.search.telemetry.SearchProviderCookie +import org.mozilla.fenix.search.telemetry.SearchProviderModel class AdsTelemetry(private val metrics: MetricController) { - private val providerList = listOf( + @VisibleForTesting + internal val providerList = listOf( SearchProviderModel( name = "google", regexp = "^https:\\/\\/www\\.google\\.(?:.+)\\/search", @@ -38,7 +46,8 @@ class AdsTelemetry(private val metrics: MetricController) { codePrefixes = listOf("ff"), followOnParams = listOf("oq", "ved", "ei"), extraAdServersRegexps = listOf( - "^https:\\/\\/duckduckgo.com\\/y\\.js" + "^https:\\/\\/duckduckgo.com\\/y\\.js", + "^https:\\/\\/www\\.amazon\\.(?:[a-z.]{2,24}).*(?:tag=duckduckgo-)" ) ), SearchProviderModel( @@ -48,7 +57,7 @@ class AdsTelemetry(private val metrics: MetricController) { ), SearchProviderModel( name = "baidu", - regexp = "^https:\\/\\/www\\.baidu\\.com\\/(?:s|baidu)", + regexp = "^https:\\/\\/www\\.baidu\\.com\\/from=844b\\/(?:s|baidu)", queryParam = "wd", codeParam = "tn", codePrefixes = listOf("34046034_", "monline_"), @@ -108,7 +117,8 @@ class AdsTelemetry(private val metrics: MetricController) { } } - private fun getProviderForUrl(url: String): SearchProviderModel? { + @VisibleForTesting + internal fun getProviderForUrl(url: String): SearchProviderModel? { for (provider in providerList) { if (Regex(provider.regexp).containsMatchIn(url)) { return provider @@ -138,7 +148,8 @@ class AdsTelemetry(private val metrics: MetricController) { } } - private inner class AdsTelemetryContentMessageHandler : MessageHandler { + @VisibleForTesting + internal inner class AdsTelemetryContentMessageHandler : MessageHandler { override fun onMessage(message: Any, source: EngineSession?): Any? { if (message is JSONObject) { @@ -164,10 +175,14 @@ class AdsTelemetry(private val metrics: MetricController) { } companion object { - private const val ADS_EXTENSION_ID = "mozacBrowserAds" - private const val ADS_EXTENSION_RESOURCE_URL = "resource://android/assets/extensions/ads/" + @VisibleForTesting + internal const val ADS_EXTENSION_ID = "mozacBrowserAds" + @VisibleForTesting + internal const val ADS_EXTENSION_RESOURCE_URL = "resource://android/assets/extensions/ads/" + @VisibleForTesting + internal const val ADS_MESSAGE_SESSION_URL_KEY = "url" + @VisibleForTesting + internal const val ADS_MESSAGE_DOCUMENT_URLS_KEY = "urls" private const val ADS_MESSAGE_ID = "MozacBrowserAds" - private const val ADS_MESSAGE_SESSION_URL_KEY = "url" - private const val ADS_MESSAGE_DOCUMENT_URLS_KEY = "urls" } } diff --git a/app/src/test/java/org/mozilla/fenix/browser/TelemetrySessionObserverTest.kt b/app/src/test/java/org/mozilla/fenix/browser/TelemetrySessionObserverTest.kt new file mode 100644 index 000000000..9987ae88f --- /dev/null +++ b/app/src/test/java/org/mozilla/fenix/browser/TelemetrySessionObserverTest.kt @@ -0,0 +1,120 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.fenix.browser + +import androidx.lifecycle.LifecycleOwner +import io.mockk.every +import io.mockk.mockk +import io.mockk.verify +import mozilla.components.browser.session.Session +import mozilla.components.browser.session.SessionManager +import org.junit.Assert +import org.junit.Before +import org.junit.Test +import org.mozilla.fenix.components.metrics.Event +import org.mozilla.fenix.components.metrics.MetricController +import org.mozilla.fenix.search.telemetry.ads.AdsTelemetry + +class TelemetrySessionObserverTest { + + private val owner: LifecycleOwner = mockk(relaxed = true) + private val sessionManager: SessionManager = mockk(relaxed = true) + private val metrics: MetricController = mockk(relaxed = true) + private val ads: AdsTelemetry = mockk(relaxed = true) + + private lateinit var singleSessionObserver: TelemetrySessionObserver + + @Before + fun setup() { + singleSessionObserver = + UriOpenedObserver(owner, sessionManager, metrics, ads).singleSessionObserver + } + + @Test + fun `tracks that a url was loaded`() { + val session: Session = mockk(relaxed = true) + every { session.url } returns "https://mozilla.com" + + singleSessionObserver.onLoadingStateChanged(session, loading = false) + verify(exactly = 0) { metrics.track(Event.UriOpened) } + + singleSessionObserver.onLoadingStateChanged(session, loading = true) + singleSessionObserver.onLoadingStateChanged(session, loading = false) + verify { metrics.track(Event.UriOpened) } + } + + @Test + fun `add originSessionUrl on first link of redirect chain and start chain`() { + val session: Session = mockk(relaxed = true) + val sessionUrl = "https://www.google.com/search" + val url = "www.aaa.com" + every { session.url } returns sessionUrl + singleSessionObserver.onLoadRequest( + session, + url, + triggeredByRedirect = false, + triggeredByWebContent = false + ) + Assert.assertEquals(sessionUrl, singleSessionObserver.originSessionUrl) + Assert.assertEquals(url, singleSessionObserver.redirectChain[0]) + } + + @Test + fun `add to redirect chain on subsequent onLoadRequests`() { + val session: Session = mockk(relaxed = true) + val url = "https://www.google.com/search" + val newUrl = "www.aaa.com" + every { session.url } returns url + singleSessionObserver.originSessionUrl = url + singleSessionObserver.redirectChain.add(url) + singleSessionObserver.onLoadRequest( + session, + newUrl, + triggeredByRedirect = false, + triggeredByWebContent = false + ) + Assert.assertEquals(url, singleSessionObserver.originSessionUrl) + Assert.assertEquals(url, singleSessionObserver.redirectChain[0]) + Assert.assertEquals(newUrl, singleSessionObserver.redirectChain[1]) + } + + @Test + fun `do nothing onLoadRequest when it's the first url of the session`() { + val session: Session = mockk(relaxed = true) + val url = "https://www.google.com/search" + every { session.url } returns url + singleSessionObserver.onLoadRequest( + session, + url, + triggeredByRedirect = false, + triggeredByWebContent = false + ) + Assert.assertNull(singleSessionObserver.originSessionUrl) + Assert.assertEquals(0, singleSessionObserver.redirectChain.size) + } + + @Test + fun `check if metric for ad clicked should be sent`() { + val session: Session = mockk(relaxed = true) + val sessionUrl = "doesn't matter" + val originSessionUrl = "https://www.google.com/search" + val url = "www.aaa.com" + every { session.url } returns sessionUrl + val redirectChain = mutableListOf(url) + singleSessionObserver.redirectChain = redirectChain + singleSessionObserver.originSessionUrl = originSessionUrl + + singleSessionObserver.onUrlChanged(session, url) + + verify { + ads.trackAdClickedMetric( + originSessionUrl, + redirectChain + ) + } + Assert.assertNull(singleSessionObserver.originSessionUrl) + Assert.assertEquals(0, singleSessionObserver.redirectChain.size) + } +} diff --git a/app/src/test/java/org/mozilla/fenix/browser/UriOpenedObserverTest.kt b/app/src/test/java/org/mozilla/fenix/browser/UriOpenedObserverTest.kt index 7e2bb8c73..43a7f8334 100644 --- a/app/src/test/java/org/mozilla/fenix/browser/UriOpenedObserverTest.kt +++ b/app/src/test/java/org/mozilla/fenix/browser/UriOpenedObserverTest.kt @@ -4,42 +4,36 @@ package org.mozilla.fenix.browser -import android.content.Context import androidx.lifecycle.LifecycleOwner -import io.mockk.every import io.mockk.mockk import io.mockk.verify import mozilla.components.browser.session.Session import mozilla.components.browser.session.SessionManager import org.junit.Before import org.junit.Test -import org.mozilla.fenix.components.metrics.Event +import org.mozilla.fenix.search.telemetry.ads.AdsTelemetry import org.mozilla.fenix.components.metrics.MetricController class UriOpenedObserverTest { - private lateinit var context: Context - private lateinit var owner: LifecycleOwner - private lateinit var sessionManager: SessionManager - private lateinit var metrics: MetricController + private val owner: LifecycleOwner = mockk(relaxed = true) + private val sessionManager: SessionManager = mockk(relaxed = true) + private val metrics: MetricController = mockk() + private val ads: AdsTelemetry = mockk() + private lateinit var observer: UriOpenedObserver @Before fun setup() { - context = mockk(relaxed = true) - owner = mockk(relaxed = true) - sessionManager = mockk(relaxed = true) - metrics = mockk(relaxed = true) + observer = UriOpenedObserver(owner, sessionManager, metrics, ads) } @Test fun `registers self as observer`() { - val observer = UriOpenedObserver(context, owner, sessionManager, metrics) verify { sessionManager.register(observer, owner) } } @Test fun `registers single session observer`() { - val observer = UriOpenedObserver(context, owner, sessionManager, metrics) val session: Session = mockk(relaxed = true) observer.onSessionAdded(session) @@ -48,18 +42,4 @@ class UriOpenedObserverTest { observer.onSessionRemoved(session) verify { session.unregister(observer.singleSessionObserver) } } - - @Test - fun `tracks that a url was loaded`() { - val observer = UriOpenedObserver(context, owner, sessionManager, metrics).singleSessionObserver - val session: Session = mockk(relaxed = true) - every { session.url } returns "https://mozilla.com" - - observer.onLoadingStateChanged(session, loading = false) - verify(exactly = 0) { metrics.track(Event.UriOpened) } - - observer.onLoadingStateChanged(session, loading = true) - observer.onLoadingStateChanged(session, loading = false) - verify { metrics.track(Event.UriOpened) } - } } diff --git a/app/src/test/java/org/mozilla/fenix/ext/AdsTest.kt b/app/src/test/java/org/mozilla/fenix/ext/AdsTest.kt new file mode 100644 index 000000000..76d89dacc --- /dev/null +++ b/app/src/test/java/org/mozilla/fenix/ext/AdsTest.kt @@ -0,0 +1,41 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.fenix.ext + +import org.junit.Assert.assertFalse +import org.junit.Assert.assertTrue +import org.junit.Test +import org.mozilla.fenix.search.telemetry.SearchProviderModel + +class AdsTest { + + private val testSearchProvider = + SearchProviderModel( + name = "test", + regexp = "test", + queryParam = "test", + codeParam = "test", + codePrefixes = listOf(), + followOnParams = listOf(), + extraAdServersRegexps = listOf( + "^https:\\/\\/www\\.bing\\.com\\/acli?c?k", + "^https:\\/\\/www\\.bing\\.com\\/fd\\/ls\\/GLinkPingPost\\.aspx.*acli?c?k" + ) + ) + + @Test + fun `test search provider contains ads`() { + val ad = "https://www.bing.com/aclick" + val nonAd = "https://www.bing.com/notanad" + assertTrue(testSearchProvider.containsAds(listOf(ad, nonAd))) + } + + @Test + fun `test search provider does not contain ads`() { + val nonAd1 = "https://www.yahoo.com/notanad" + val nonAd2 = "https://www.google.com/" + assertFalse(testSearchProvider.containsAds(listOf(nonAd1, nonAd2))) + } +} diff --git a/app/src/test/java/org/mozilla/fenix/search/telemetry/ads/AdsTelemetryTest.kt b/app/src/test/java/org/mozilla/fenix/search/telemetry/ads/AdsTelemetryTest.kt new file mode 100644 index 000000000..def2b5ce9 --- /dev/null +++ b/app/src/test/java/org/mozilla/fenix/search/telemetry/ads/AdsTelemetryTest.kt @@ -0,0 +1,128 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.fenix.search.telemetry.ads + +import io.mockk.mockk +import io.mockk.slot +import io.mockk.spyk +import io.mockk.verify +import mozilla.components.browser.state.store.BrowserStore +import mozilla.components.concept.engine.Engine +import org.json.JSONArray +import org.json.JSONObject +import org.junit.Assert.assertEquals +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mozilla.fenix.components.metrics.Event +import org.mozilla.fenix.components.metrics.MetricController +import org.mozilla.fenix.helpers.FenixRobolectricTestRunner +import org.mozilla.fenix.search.telemetry.ads.AdsTelemetry.Companion.ADS_EXTENSION_ID +import org.mozilla.fenix.search.telemetry.ads.AdsTelemetry.Companion.ADS_EXTENSION_RESOURCE_URL +import org.mozilla.fenix.search.telemetry.ads.AdsTelemetry.Companion.ADS_MESSAGE_DOCUMENT_URLS_KEY +import org.mozilla.fenix.search.telemetry.ads.AdsTelemetry.Companion.ADS_MESSAGE_SESSION_URL_KEY + +@RunWith(FenixRobolectricTestRunner::class) +class AdsTelemetryTest { + + private val metrics: MetricController = mockk(relaxed = true) + private lateinit var ads: AdsTelemetry + private lateinit var adsMessageHandler: AdsTelemetry.AdsTelemetryContentMessageHandler + + @Before + fun setUp() { + ads = spyk(AdsTelemetry(metrics)) + adsMessageHandler = ads.AdsTelemetryContentMessageHandler() + } + + @Test + fun `don't track with null session url`() { + ads.trackAdClickedMetric(null, listOf()) + + verify(exactly = 0) { ads.getProviderForUrl(any()) } + } + + @Test + fun `don't track when no ads are in the redirect path`() { + val sessionUrl = "https://www.google.com/search?q=aaa" + + ads.trackAdClickedMetric(sessionUrl, listOf("https://www.aaa.com")) + + verify(exactly = 0) { metrics.track(any()) } + } + + @Test + fun `track when ads are in the redirect path`() { + val metricEvent = slot() + val sessionUrl = "https://www.google.com/search?q=aaa" + + ads.trackAdClickedMetric( + sessionUrl, + listOf("https://www.google.com/aclk", "https://www.aaa.com") + ) + + verify { metrics.track(capture(metricEvent)) } + assertEquals(ads.providerList[0].name, metricEvent.captured.label) + } + + @Test + fun install() { + val engine = mockk(relaxed = true) + val store = mockk(relaxed = true) + + ads.install(engine, store) + + verify { + engine.installWebExtension( + id = ADS_EXTENSION_ID, + url = ADS_EXTENSION_RESOURCE_URL, + allowContentMessaging = true, + onSuccess = any(), + onError = any() + ) + } + } + + @Test + fun `message handler processes the document urls and reports an ad`() { + val metricEvent = slot() + val first = "https://www.google.com/aclk" + val second = "https://www.google.com/aaa" + val array = JSONArray() + array.put(first) + array.put(second) + val message = JSONObject() + message.put(ADS_MESSAGE_DOCUMENT_URLS_KEY, array) + message.put(ADS_MESSAGE_SESSION_URL_KEY, "https://www.google.com/search?q=aaa") + + assertEquals("", adsMessageHandler.onMessage(message, mockk())) + + verify { metrics.track(capture(metricEvent)) } + assertEquals(ads.providerList[0].name, metricEvent.captured.label) + } + + @Test + fun `message handler processes the document urls and doesn't find ads`() { + val first = "https://www.google.com/aaaaaa" + val second = "https://www.google.com/aaa" + val array = JSONArray() + array.put(first) + array.put(second) + val message = JSONObject() + message.put(ADS_MESSAGE_DOCUMENT_URLS_KEY, array) + message.put(ADS_MESSAGE_SESSION_URL_KEY, "https://www.google.com/search?q=aaa") + + assertEquals("", adsMessageHandler.onMessage(message, mockk())) + + verify(exactly = 0) { metrics.track(any()) } + } + + @Test(expected = IllegalStateException::class) + fun `message handler finds no json object`() { + val message = "message" + + adsMessageHandler.onMessage(message, mockk()) + } +} diff --git a/docs/metrics.md b/docs/metrics.md index 982c37201..9f193fcec 100644 --- a/docs/metrics.md +++ b/docs/metrics.md @@ -46,8 +46,8 @@ The following metrics are added to the ping: | Name | Type | Description | Data reviews | Extras | Expiration | | --- | --- | --- | --- | --- | --- | -| browser.search.ad_clicks |[labeled_counter](https://mozilla.github.io/glean/book/user/metrics/labeled_counters.html) |Records clicks of adverts on SERP pages. The key format is ‘’. |[1](https://github.com/mozilla-mobile/fenix/issues/6558)||2020-09-01 | -| browser.search.with_ads |[labeled_counter](https://mozilla.github.io/glean/book/user/metrics/labeled_counters.html) |Records counts of SERP pages with adverts displayed. The key format is ‘’. |[1](https://github.com/mozilla-mobile/fenix/issues/6558)||2020-09-01 | +| browser.search.ad_clicks |[labeled_counter](https://mozilla.github.io/glean/book/user/metrics/labeled_counters.html) |Records clicks of adverts on SERP pages. The key format is ‘’. |[1](https://github.com/mozilla-mobile/fenix/pull/10112)||2020-09-01 | +| browser.search.with_ads |[labeled_counter](https://mozilla.github.io/glean/book/user/metrics/labeled_counters.html) |Records counts of SERP pages with adverts displayed. The key format is ‘’. |[1](https://github.com/mozilla-mobile/fenix/pull/10112)||2020-09-01 | | events.total_uri_count |[counter](https://mozilla.github.io/glean/book/user/metrics/counter.html) |A counter of URIs visited by the user in the current session, including page reloads. This does not include background page requests and URIs from embedded pages or private browsing. |[1](https://github.com/mozilla-mobile/fenix/pull/1785), [2](https://github.com/mozilla-mobile/fenix/pull/8314)||2020-09-01 | | metrics.search_count |[labeled_counter](https://mozilla.github.io/glean/book/user/metrics/labeled_counters.html) |The labels for this counter are `.`. If the search engine is bundled with Fenix `search-engine-name` will be the name of the search engine. If it's a custom search engine (defined: https://github.com/mozilla-mobile/fenix/issues/1607) the value will be `custom`. `source` will be: `action`, `suggestion`, `widget` or `shortcut` (depending on the source from which the search started). Also added the `other` option for the source but it should never enter on this case. |[1](https://github.com/mozilla-mobile/fenix/pull/1677), [2](https://github.com/mozilla-mobile/fenix/pull/5216), [3](https://github.com/mozilla-mobile/fenix/pull/7310)||2020-09-01 | @@ -234,6 +234,8 @@ The following metrics are added to the ping: | --- | --- | --- | --- | --- | --- | | addons.has_enabled_addons |[boolean](https://mozilla.github.io/glean/book/user/metrics/boolean.html) |Whether or not the user has enabled add-ons on the device. |[1](https://github.com/mozilla-mobile/fenix/pull/8318)||2020-09-01 | | addons.has_installed_addons |[boolean](https://mozilla.github.io/glean/book/user/metrics/boolean.html) |Whether or not the user has installed add-ons on the device. |[1](https://github.com/mozilla-mobile/fenix/pull/8318)||2020-09-01 | +| browser.search.ad_clicks |[labeled_counter](https://mozilla.github.io/glean/book/user/metrics/labeled_counters.html) |Records clicks of adverts on SERP pages. The key format is ‘’. |[1](https://github.com/mozilla-mobile/fenix/pull/10112)||2020-09-01 | +| browser.search.with_ads |[labeled_counter](https://mozilla.github.io/glean/book/user/metrics/labeled_counters.html) |Records counts of SERP pages with adverts displayed. The key format is ‘’. |[1](https://github.com/mozilla-mobile/fenix/pull/10112)||2020-09-01 | | events.total_uri_count |[counter](https://mozilla.github.io/glean/book/user/metrics/counter.html) |A counter of URIs visited by the user in the current session, including page reloads. This does not include background page requests and URIs from embedded pages or private browsing. |[1](https://github.com/mozilla-mobile/fenix/pull/1785), [2](https://github.com/mozilla-mobile/fenix/pull/8314)||2020-09-01 | | metrics.adjust_ad_group |[string](https://mozilla.github.io/glean/book/user/metrics/string.html) |A string containing the Adjust ad group ID from which the user installed Fenix. This will not send on the first session the user runs. If the install is organic, this will be empty. |[1](https://github.com/mozilla-mobile/fenix/pull/9253)||2020-09-01 | | metrics.adjust_campaign |[string](https://mozilla.github.io/glean/book/user/metrics/string.html) |A string containing the Adjust campaign ID from which the user installed Fenix. This will not send on the first session the user runs. If the install is organic, this will be empty. |[1](https://github.com/mozilla-mobile/fenix/pull/5579)||2020-09-01 |