1
0
Fork 0

For #6558 - cleanup + added unit tests

master
Mihai Branescu 2020-04-22 11:46:40 +03:00 committed by Jeff Boek
parent f03d65b13d
commit c4d76dce5a
16 changed files with 393 additions and 76 deletions

View File

@ -2007,11 +2007,12 @@ browser.search:
description: >
Records counts of SERP pages with adverts displayed. The key format is <provider-name>.
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 <provider-name>.
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"

View File

@ -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);
};

View File

@ -1,10 +0,0 @@
package org.mozilla.fenix.ads
data class SearchProviderCookie(
val extraCodeParam: String,
val extraCodePrefixes: List<String>,
val host: String,
val name: String,
val codeParam: String,
val codePrefixes: List<String>
)

View File

@ -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,

View File

@ -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

View File

@ -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

View File

@ -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<String>): Boolean {
return urlList.containsAds(this.extraAdServersRegexps)

View File

@ -1,12 +0,0 @@
package org.mozilla.fenix.ext
import org.json.JSONArray
@Suppress("UNCHECKED_CAST")
fun <T> JSONArray.toList(): List<T> {
val result = ArrayList<T>()
for (i in 0 until length()) {
result.add(get(i) as T)
}
return result
}

View File

@ -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<String>,
val host: String,
val name: String,
val codeParam: String,
val codePrefixes: List<String>
)

View File

@ -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,

View File

@ -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"
}
}

View File

@ -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)
}
}

View File

@ -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) }
}
}

View File

@ -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)))
}
}

View File

@ -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<Event.SearchAdClicked>()
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<Engine>(relaxed = true)
val store = mockk<BrowserStore>(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<Event.SearchWithAds>()
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())
}
}

File diff suppressed because one or more lines are too long