For #20229 - Use the AC common implementation for ads/search telemetry

Everything should work exactly as before.
upstream-sync
Mugurell 3 years ago
parent b2a5723bad
commit f95567912f

@ -741,14 +741,3 @@ ext.updateExtensionVersion = { task, extDir ->
expand(values)
}
}
tasks.register("updateAdsExtensionVersion", Copy) { task ->
updateExtensionVersion(task, 'src/main/assets/extensions/ads')
}
tasks.register("updateCookiesExtensionVersion", Copy) { task ->
updateExtensionVersion(task, 'src/main/assets/extensions/cookies')
}
preBuild.dependsOn "updateAdsExtensionVersion"
preBuild.dependsOn "updateCookiesExtensionVersion"

@ -4235,11 +4235,12 @@ browser.search:
- https://github.com/mozilla-mobile/fenix/pull/10112
- https://github.com/mozilla-mobile/fenix/pull/15713#issuecomment-703972068
- https://github.com/mozilla-mobile/fenix/pull/19924#issuecomment-861423789
- https://github.com/mozilla-mobile/focus-android/pull/4968#issuecomment-879256443
data_sensitivity:
- interaction
notification_emails:
- android-probes@mozilla.com
expires: "2021-08-01"
expires: "2022-07-01"
ad_clicks:
type: labeled_counter
description: |
@ -4253,11 +4254,12 @@ browser.search:
- https://github.com/mozilla-mobile/fenix/pull/10112
- https://github.com/mozilla-mobile/fenix/pull/15713#issuecomment-703972068
- https://github.com/mozilla-mobile/fenix/pull/19924#issuecomment-861423789
- https://github.com/mozilla-mobile/focus-android/pull/4968#issuecomment-879256443
data_sensitivity:
- interaction
notification_emails:
- android-probes@mozilla.com
expires: "2021-08-01"
expires: "2022-07-01"
in_content:
type: labeled_counter
description: |
@ -4270,11 +4272,12 @@ browser.search:
- https://github.com/mozilla-mobile/fenix/pull/10167
- https://github.com/mozilla-mobile/fenix/pull/15713#issuecomment-703972068
- https://github.com/mozilla-mobile/fenix/pull/19924#issuecomment-861423789
- https://github.com/mozilla-mobile/focus-android/pull/4968#issuecomment-879256443
data_sensitivity:
- interaction
notification_emails:
- android-probes@mozilla.com
expires: "2021-08-01"
expires: "2022-07-01"
addons:
open_addons_in_settings:

@ -1,61 +0,0 @@
/* 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) {
continue;
}
urls.push(anchor.href);
}
}
function sendLinks(cookies) {
let urls = [];
collectLinks(urls);
let message = {
'url': document.location.href,
'urls': urls,
'cookies': cookies
};
browser.runtime.sendNativeMessage("MozacBrowserAds", message);
}
function notify(message) {
sendLinks(message.cookies);
}
browser.runtime.onMessage.addListener(notify);
const events = ["pageshow", "load", "unload"];
var timeout;
const eventLogger = event => {
switch (event.type) {
case "load":
timeout = setTimeout(() => {
browser.runtime.sendMessage({ "checkCookies": true });
}, ADLINK_CHECK_TIMEOUT_MS)
break;
case "pageshow":
if (event.persisted) {
timeout = setTimeout(() => {
browser.runtime.sendMessage({ "checkCookies": true });
}, ADLINK_CHECK_TIMEOUT_MS)
}
break;
case "unload":
clearTimeout(timeout);
default:
console.log('Event:', event.type);
}
};
events.forEach(eventName =>
window.addEventListener(eventName, eventLogger)
);

@ -1,28 +0,0 @@
/* 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/. */
browser.runtime.onMessage.addListener(notify);
function sendMessageToTabs(tabs, cookies) {
for (let tab of tabs) {
browser.tabs.sendMessage(
tab.id,
{ cookies }
);
}
}
function notify(message) {
if (message.checkCookies) {
browser.cookies.getAll({})
.then(cookies => {
browser.tabs.query({
currentWindow: true,
active: true
}).then(tabs => {
sendMessageToTabs(tabs, cookies);
});
});
}
}

@ -1,40 +0,0 @@
{
"manifest_version": 2,
"applications": {
"gecko": {
"id": "ads@mozac.org"
}
},
"name": "Mozilla Android Components - Ads",
"version": "${version}",
"content_scripts": [
{
"matches": ["https://*/*"],
"include_globs": [
"https://www.google.*/search*",
"https://www.bing.com/search*",
"https://www.baidu.com/*",
"https://m.baidu.com/*",
"https://duckduckgo.com/*"
],
"js": ["ads.js"],
"run_at": "document_end"
}
],
"background": {
"scripts": ["adsBackground.js"]
},
"permissions": [
"geckoViewAddons",
"nativeMessaging",
"nativeMessagingFromContent",
"geckoViewAddons",
"nativeMessaging",
"nativeMessagingFromContent",
"webNavigation",
"webRequest",
"webRequestBlocking",
"cookies",
"*://*/*"
]
}

@ -1,47 +0,0 @@
/* 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 COOKIES_CHECK_TIMEOUT_MS = 1000;
function sendCookies(cookies) {
let message = {
'url': document.location.href,
'cookies': cookies
}
browser.runtime.sendNativeMessage("BrowserCookiesMessage", message);
}
function notify(message) {
sendCookies(message.cookies);
}
browser.runtime.onMessage.addListener(notify);
const events = ["pageshow", "load", "unload"];
var timeout;
const eventLogger = event => {
switch (event.type) {
case "load":
timeout = setTimeout(() => {
browser.runtime.sendMessage({"checkCookies": true});
}, COOKIES_CHECK_TIMEOUT_MS);
break;
case "pageshow":
if (event.persisted) {
timeout = setTimeout(() => {
browser.runtime.sendMessage({"checkCookies": true});
}, COOKIES_CHECK_TIMEOUT_MS);
}
break;
case "unload":
clearTimeout(timeout);
default:
console.log('Event:', event.type);
}
};
events.forEach(eventName =>
window.addEventListener(eventName, eventLogger)
);

@ -1,28 +0,0 @@
/* 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/. */
browser.runtime.onMessage.addListener(notify);
function sendMessageToTabs(tabs, cookies) {
for (let tab of tabs) {
browser.tabs.sendMessage(
tab.id,
{cookies: cookies}
);
}
}
function notify(message) {
if(message.checkCookies) {
browser.cookies.getAll({})
.then(cookies => {
browser.tabs.query({
currentWindow: true,
active: true
}).then(tabs => {
sendMessageToTabs(tabs, cookies);
});
});
}
}

@ -1,38 +0,0 @@
{
"manifest_version": 2,
"applications": {
"gecko": {
"id": "cookies@mozac.org"
}
},
"name": "Mozilla Android Components - Cookies",
"version": "${version}",
"content_scripts": [
{
"matches": ["https://*/*"],
"include_globs": [
"https://www.google.*/search*",
"https://www.baidu.com/*",
"https://m.baidu.com/*",
"https://*search.yahoo.com/search*",
"https://www.bing.com/search*",
"https://duckduckgo.com/*"
],
"js": ["cookies.js"],
"run_at": "document_end"
}
],
"background": {
"scripts": ["cookiesBackground.js"]
},
"permissions": [
"geckoViewAddons",
"nativeMessaging",
"nativeMessagingFromContent",
"webNavigation",
"webRequest",
"webRequestBlocking",
"cookies",
"*://*/*"
]
}

@ -39,8 +39,11 @@ import mozilla.components.feature.pwa.ManifestStorage
import mozilla.components.feature.pwa.WebAppShortcutManager
import mozilla.components.feature.readerview.ReaderViewMiddleware
import mozilla.components.feature.recentlyclosed.RecentlyClosedMiddleware
import mozilla.components.feature.search.middleware.AdsTelemetryMiddleware
import mozilla.components.feature.search.middleware.SearchMiddleware
import mozilla.components.feature.search.region.RegionMiddleware
import mozilla.components.feature.search.telemetry.ads.AdsTelemetry
import mozilla.components.feature.search.telemetry.incontent.InContentTelemetry
import mozilla.components.feature.session.HistoryDelegate
import mozilla.components.feature.session.middleware.LastAccessMiddleware
import mozilla.components.feature.session.middleware.undo.UndoMiddleware
@ -76,8 +79,6 @@ import org.mozilla.fenix.historymetadata.HistoryMetadataService
import org.mozilla.fenix.media.MediaSessionService
import org.mozilla.fenix.perf.StrictModeManager
import org.mozilla.fenix.perf.lazyMonitored
import org.mozilla.fenix.search.telemetry.ads.AdsTelemetry
import org.mozilla.fenix.search.telemetry.incontent.InContentTelemetry
import org.mozilla.fenix.settings.SupportUtils
import org.mozilla.fenix.settings.advanced.getSelectedLocale
import org.mozilla.fenix.telemetry.TelemetryMiddleware
@ -195,7 +196,6 @@ class Core(
ReaderViewMiddleware(),
TelemetryMiddleware(
context.settings(),
adsTelemetry,
metrics
),
ThumbnailsMiddleware(thumbnailStorage),
@ -207,7 +207,8 @@ class Core(
migration = SearchMigration(context)
),
RecordingDevicesMiddleware(context),
PromptMiddleware()
PromptMiddleware(),
AdsTelemetryMiddleware(adsTelemetry)
)
if (FeatureFlags.historyMetadataFeature) {
@ -268,11 +269,11 @@ class Core(
}
val adsTelemetry by lazyMonitored {
AdsTelemetry(metrics)
AdsTelemetry()
}
val searchTelemetry by lazyMonitored {
InContentTelemetry(metrics)
InContentTelemetry()
}
/**

@ -23,6 +23,8 @@ import mozilla.components.feature.media.facts.MediaFacts
import mozilla.components.feature.prompts.facts.LoginDialogFacts
import mozilla.components.feature.prompts.facts.CreditCardAutofillDialogFacts
import mozilla.components.feature.pwa.ProgressiveWebAppFacts
import mozilla.components.feature.search.telemetry.ads.AdsTelemetry
import mozilla.components.feature.search.telemetry.incontent.InContentTelemetry
import mozilla.components.feature.syncedtabs.facts.SyncedTabsFacts
import mozilla.components.feature.top.sites.facts.TopSitesFacts
import mozilla.components.lib.dataprotect.SecurePrefsReliabilityExperiment
@ -315,6 +317,15 @@ internal class ReleaseMetricController(
Event.SecurePrefsReset
}
Component.FEATURE_SEARCH to AdsTelemetry.SERP_ADD_CLICKED -> {
Event.SearchAdClicked(value!!)
}
Component.FEATURE_SEARCH to AdsTelemetry.SERP_SHOWN_WITH_ADDS -> {
Event.SearchWithAds(value!!)
}
Component.FEATURE_SEARCH to InContentTelemetry.IN_CONTENT_SEARCH -> {
Event.SearchInContent(value!!)
}
else -> null
}

@ -1,154 +0,0 @@
/* 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
import androidx.annotation.VisibleForTesting
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.map
import mozilla.components.browser.state.state.BrowserState
import mozilla.components.browser.state.store.BrowserStore
import mozilla.components.concept.engine.Engine
import mozilla.components.concept.engine.EngineSession
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
abstract class BaseSearchTelemetry {
@VisibleForTesting
internal val providerList = listOf(
SearchProviderModel(
name = "google",
regexp = "^https:\\/\\/www\\.google\\.(?:.+)\\/search",
queryParam = "q",
codeParam = "client",
codePrefixes = listOf("firefox"),
followOnParams = listOf("oq", "ved", "ei"),
extraAdServersRegexps = listOf("^https?:\\/\\/www\\.google(?:adservices)?\\.com\\/(?:pagead\\/)?aclk")
),
SearchProviderModel(
name = "duckduckgo",
regexp = "^https:\\/\\/duckduckgo\\.com\\/",
queryParam = "q",
codeParam = "t",
codePrefixes = listOf("f"),
extraAdServersRegexps = listOf(
"^https:\\/\\/duckduckgo.com\\/y\\.js",
"^https:\\/\\/www\\.amazon\\.(?:[a-z.]{2,24}).*(?:tag=duckduckgo-)"
)
),
SearchProviderModel(
name = "yahoo",
regexp = "^https:\\/\\/(?:.*)search\\.yahoo\\.com\\/search",
queryParam = "p"
),
SearchProviderModel(
name = "baidu",
regexp = "^https:\\/\\/m\\.baidu\\.com(?:.*)\\/s",
queryParam = "word",
codeParam = "from",
codePrefixes = listOf("1000969a"),
followOnParams = listOf("oq")
),
SearchProviderModel(
name = "bing",
regexp = "^https:\\/\\/www\\.bing\\.com\\/search",
queryParam = "q",
codeParam = "pc",
codePrefixes = listOf("MOZ", "MZ"),
followOnCookies = listOf(
SearchProviderCookie(
extraCodeParam = "form",
extraCodePrefixes = listOf("QBRE"),
host = "www.bing.com",
name = "SRCHS",
codeParam = "PC",
codePrefixes = listOf("MOZ", "MZ")
)
),
extraAdServersRegexps = listOf(
"^https:\\/\\/www\\.bing\\.com\\/acli?c?k",
"^https:\\/\\/www\\.bing\\.com\\/fd\\/ls\\/GLinkPingPost\\.aspx.*acli?c?k"
)
)
)
abstract fun install(engine: Engine, store: BrowserStore)
internal fun getProviderForUrl(url: String): SearchProviderModel? =
providerList.find { provider -> provider.regexp.containsMatchIn(url) }
@OptIn(ExperimentalCoroutinesApi::class)
internal fun installWebExtension(
engine: Engine,
store: BrowserStore,
extensionInfo: ExtensionInfo
) {
engine.installWebExtension(
id = extensionInfo.id,
url = extensionInfo.resourceUrl,
onSuccess = { extension ->
store.flowScoped { flow -> subscribeToUpdates(flow, extension, extensionInfo) }
},
onError = { _, throwable ->
Logger.error("Could not install ${extensionInfo.id} extension", throwable)
})
}
private suspend fun subscribeToUpdates(
flow: Flow<BrowserState>,
extension: WebExtension,
extensionInfo: ExtensionInfo
) {
// Whenever we see a new EngineSession in the store then we register our content message
// handler if it has not been added yet.
flow.map { it.tabs }
.filterChanged { it.engineState.engineSession }
.collect { state ->
val engineSession = state.engineState.engineSession ?: return@collect
if (extension.hasContentMessageHandler(engineSession, extensionInfo.messageId)) {
return@collect
}
extension.registerContentMessageHandler(
engineSession,
extensionInfo.messageId,
SearchTelemetryMessageHandler()
)
}
}
protected fun <T> getMessageList(message: JSONObject, key: String): List<T> {
return message.getJSONArray(key).toList()
}
/**
* This method is used to process any valid json message coming from a web-extension
*/
@VisibleForTesting
internal abstract fun processMessage(message: JSONObject)
@VisibleForTesting
internal inner class SearchTelemetryMessageHandler : MessageHandler {
override fun onMessage(message: Any, source: EngineSession?): Any? {
if (message is JSONObject) {
processMessage(message)
} else {
throw IllegalStateException("Received unexpected message: $message")
}
// Needs to return something that is not null and not Unit:
// https://github.com/mozilla-mobile/android-components/issues/2969
return ""
}
}
}

@ -1,11 +0,0 @@
/* 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 ExtensionInfo(
internal val id: String,
internal val resourceUrl: String,
internal val messageId: String
)

@ -1,14 +0,0 @@
/* 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>
)

@ -1,46 +0,0 @@
/* 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,
val regexp: Regex,
val queryParam: String,
val codeParam: String,
val codePrefixes: List<String>,
val followOnParams: List<String>,
val extraAdServersRegexps: List<Regex>,
val followOnCookies: List<SearchProviderCookie>
) {
constructor(
name: String,
regexp: String,
queryParam: String,
codeParam: String = "",
codePrefixes: List<String> = emptyList(),
followOnParams: List<String> = emptyList(),
extraAdServersRegexps: List<String> = emptyList(),
followOnCookies: List<SearchProviderCookie> = emptyList()
) : this(
name = name,
regexp = regexp.toRegex(),
queryParam = queryParam,
codeParam = codeParam,
codePrefixes = codePrefixes,
followOnParams = followOnParams,
extraAdServersRegexps = extraAdServersRegexps.map { it.toRegex() },
followOnCookies = followOnCookies
)
/**
* Checks if any of the given URLs represent an ad from the search engine.
* Used to check if a clicked link was for an ad.
*/
fun containsAdLinks(urlList: List<String>) = urlList.any { url -> isAd(url) }
private fun isAd(url: String) =
extraAdServersRegexps.any { adsRegex -> adsRegex.containsMatchIn(url) }
}

@ -1,36 +0,0 @@
/* 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
import java.util.Locale
/**
* A data class that tracks key information about a Search Engine Result Page (SERP).
*
* @property provider The name of the search provider.
* @property type The search access point type (SAP). This is either "organic", "sap" or
* "sap-follow-on".
* @property code The search URL's `code` query parameter.
* @property channel The search URL's `channel` query parameter.
*/
internal data class TrackKeyInfo(
var provider: String,
var type: String,
var code: String?,
var channel: String? = null
) {
/**
* Returns the track key information into the following string format:
* `<provider>.in-content.[sap|sap-follow-on|organic].[code|none](.[channel])?`.
*/
fun createTrackKey(): String {
return "${provider.toLowerCase(Locale.ROOT)}.in-content" +
".${type.toLowerCase(Locale.ROOT)}" +
".${code?.toLowerCase(Locale.ROOT) ?: "none"}" +
if (!channel?.toLowerCase(Locale.ROOT).isNullOrBlank())
".${channel?.toLowerCase(Locale.ROOT)}"
else ""
}
}

@ -1,110 +0,0 @@
/* 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
import android.net.Uri
import org.json.JSONObject
private const val SEARCH_TYPE_SAP_FOLLOW_ON = "sap-follow-on"
private const val SEARCH_TYPE_SAP = "sap"
private const val SEARCH_TYPE_ORGANIC = "organic"
private const val CHANNEL_KEY = "channel"
internal fun getTrackKey(
provider: SearchProviderModel,
uri: Uri,
cookies: List<JSONObject>
): String {
val paramSet = uri.queryParameterNames
var code: String? = null
if (provider.codeParam.isNotEmpty()) {
code = uri.getQueryParameter(provider.codeParam)
if (code.isNullOrEmpty() &&
provider.name == "baidu" &&
uri.toString().contains("from=")) {
code = uri.toString().substringAfter("from=", "")
.substringBefore("/", "")
}
// Glean doesn't allow code starting with a figure
if (code != null && code.isNotEmpty()) {
val codeStart = code.first()
if (codeStart.isDigit()) {
code = "_$code"
}
}
// Try cookies first because Bing has followOnCookies and valid code, but no
// followOnParams => would tracks organic instead of sap-follow-on
if (provider.followOnCookies.isNotEmpty()) {
// Checks if engine contains a valid follow-on cookie, otherwise return default
getTrackKeyFromCookies(provider, uri, cookies)?.let {
return it.createTrackKey()
}
}
// For Bing if it didn't have a valid cookie and for all the other search engines
if (hasValidCode(uri.getQueryParameter(provider.codeParam), provider)) {
val channel = uri.getQueryParameter(CHANNEL_KEY)
val type = getSapType(provider.followOnParams, paramSet)
return TrackKeyInfo(provider.name, type, code, channel).createTrackKey()
}
}
// Default to organic search type if no code parameter was found.
return TrackKeyInfo(provider.name, SEARCH_TYPE_ORGANIC, code).createTrackKey()
}
private fun getTrackKeyFromCookies(
provider: SearchProviderModel,
uri: Uri,
cookies: List<JSONObject>
): TrackKeyInfo? {
// Especially Bing requires lots of extra work related to cookies.
for (followOnCookie in provider.followOnCookies) {
val eCode = uri.getQueryParameter(followOnCookie.extraCodeParam)
if (eCode == null || !followOnCookie.extraCodePrefixes.any { prefix ->
eCode.startsWith(prefix)
}) {
continue
}
// If this cookie is present, it's probably an SAP follow-on.
// This might be an organic follow-on in the same session, but there
// is no way to tell the difference.
for (cookie in cookies) {
if (cookie.getString("name") != followOnCookie.name) {
continue
}
val valueList = cookie.getString("value")
.split("=")
.map { item -> item.trim() }
if (valueList.size == 2 && valueList[0] == followOnCookie.codeParam &&
followOnCookie.codePrefixes.any { prefix ->
valueList[1].startsWith(
prefix
)
}
) {
return TrackKeyInfo(provider.name, SEARCH_TYPE_SAP_FOLLOW_ON, valueList[1])
}
}
}
return null
}
private fun getSapType(followOnParams: List<String>, paramSet: Set<String>): String {
return if (followOnParams.any { param -> paramSet.contains(param) }) {
SEARCH_TYPE_SAP_FOLLOW_ON
} else {
SEARCH_TYPE_SAP
}
}
private fun hasValidCode(code: String?, provider: SearchProviderModel): Boolean =
code != null && provider.codePrefixes.any { prefix -> code.startsWith(prefix) }

@ -1,96 +0,0 @@
/* 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 androidx.core.net.toUri
import mozilla.components.browser.state.store.BrowserStore
import mozilla.components.concept.engine.Engine
import org.json.JSONObject
import org.mozilla.fenix.components.metrics.Event
import org.mozilla.fenix.components.metrics.MetricController
import org.mozilla.fenix.search.telemetry.BaseSearchTelemetry
import org.mozilla.fenix.search.telemetry.ExtensionInfo
import org.mozilla.fenix.search.telemetry.getTrackKey
class AdsTelemetry(private val metrics: MetricController) : BaseSearchTelemetry() {
// Cache the cookies provided by the ADS_EXTENSION_ID extension to be used when tracking
// the Ads clicked telemetry.
var cachedCookies = listOf<JSONObject>()
override fun install(
engine: Engine,
store: BrowserStore
) {
val info = ExtensionInfo(
id = ADS_EXTENSION_ID,
resourceUrl = ADS_EXTENSION_RESOURCE_URL,
messageId = ADS_MESSAGE_ID
)
installWebExtension(engine, store, info)
}
override fun processMessage(message: JSONObject) {
// Cache the cookies list when the extension sends a message.
cachedCookies = getMessageList(
message,
ADS_MESSAGE_COOKIES_KEY
)
val urls = getMessageList<String>(message, ADS_MESSAGE_DOCUMENT_URLS_KEY)
val provider = getProviderForUrl(message.getString(ADS_MESSAGE_SESSION_URL_KEY))
provider?.let {
if (it.containsAdLinks(urls)) {
metrics.track(Event.SearchWithAds(it.name))
}
}
}
/**
* If a search ad is clicked, record the search ad that was clicked. This method is called
* when the browser is navigating to a new URL, which may be a search ad.
*
* @param url The URL of the page before the search ad was clicked. This is used to determine
* the originating search provider.
* @param urlPath A list of the URLs and load requests collected in between location changes.
* Clicking on a search ad generates a list of redirects from the originating search provider
* to the ad source. This is used to determine if there was an ad click.
*/
fun trackAdClickedMetric(url: String?, urlPath: List<String>) {
val uri = url?.toUri() ?: return
val provider = getProviderForUrl(url) ?: return
val paramSet = uri.queryParameterNames
if (!paramSet.contains(provider.queryParam) || !provider.containsAdLinks(urlPath)) {
// Do nothing if the URL does not have the search provider's query parameter or
// there were no ad clicks.
return
}
metrics.track(Event.SearchAdClicked(getTrackKey(provider, uri, cachedCookies)))
}
companion object {
@VisibleForTesting
internal const val ADS_EXTENSION_ID = "ads@mozac.org"
@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"
@VisibleForTesting
internal const val ADS_MESSAGE_ID = "MozacBrowserAds"
@VisibleForTesting
internal const val ADS_MESSAGE_COOKIES_KEY = "cookies"
}
}

@ -1,67 +0,0 @@
/* 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.incontent
import androidx.annotation.VisibleForTesting
import androidx.core.net.toUri
import mozilla.components.browser.state.store.BrowserStore
import mozilla.components.concept.engine.Engine
import org.json.JSONObject
import org.mozilla.fenix.components.metrics.Event
import org.mozilla.fenix.components.metrics.MetricController
import org.mozilla.fenix.search.telemetry.BaseSearchTelemetry
import org.mozilla.fenix.search.telemetry.ExtensionInfo
import org.mozilla.fenix.search.telemetry.getTrackKey
class InContentTelemetry(private val metrics: MetricController) : BaseSearchTelemetry() {
override fun install(engine: Engine, store: BrowserStore) {
val info = ExtensionInfo(
id = COOKIES_EXTENSION_ID,
resourceUrl = COOKIES_EXTENSION_RESOURCE_URL,
messageId = COOKIES_MESSAGE_ID
)
installWebExtension(engine, store, info)
}
override fun processMessage(message: JSONObject) {
val cookies = getMessageList<JSONObject>(
message,
COOKIES_MESSAGE_LIST_KEY
)
trackPartnerUrlTypeMetric(message.getString(COOKIES_MESSAGE_SESSION_URL_KEY), cookies)
}
@VisibleForTesting
internal fun trackPartnerUrlTypeMetric(url: String, cookies: List<JSONObject>) {
val provider = getProviderForUrl(url) ?: return
val uri = url.toUri()
val paramSet = uri.queryParameterNames
if (!paramSet.contains(provider.queryParam)) {
return
}
metrics.track(Event.SearchInContent(getTrackKey(provider, uri, cookies)))
}
companion object {
@VisibleForTesting
internal const val COOKIES_EXTENSION_ID = "cookies@mozac.org"
@VisibleForTesting
internal const val COOKIES_EXTENSION_RESOURCE_URL =
"resource://android/assets/extensions/cookies/"
@VisibleForTesting
internal const val COOKIES_MESSAGE_SESSION_URL_KEY = "url"
@VisibleForTesting
internal const val COOKIES_MESSAGE_LIST_KEY = "cookies"
@VisibleForTesting
internal const val COOKIES_MESSAGE_ID = "BrowserCookiesMessage"
}
}

@ -4,7 +4,6 @@
package org.mozilla.fenix.telemetry
import androidx.annotation.VisibleForTesting
import mozilla.components.browser.state.action.BrowserAction
import mozilla.components.browser.state.action.ContentAction
import mozilla.components.browser.state.action.DownloadAction
@ -16,13 +15,13 @@ import mozilla.components.browser.state.selector.normalTabs
import mozilla.components.browser.state.state.BrowserState
import mozilla.components.browser.state.state.EngineState
import mozilla.components.browser.state.state.SessionState
import mozilla.components.feature.search.telemetry.ads.AdsTelemetry
import mozilla.components.lib.state.Middleware
import mozilla.components.lib.state.MiddlewareContext
import mozilla.components.support.base.android.Clock
import mozilla.components.support.base.log.logger.Logger
import org.mozilla.fenix.components.metrics.Event
import org.mozilla.fenix.components.metrics.MetricController
import org.mozilla.fenix.search.telemetry.ads.AdsTelemetry
import org.mozilla.fenix.utils.Settings
import org.mozilla.fenix.GleanMetrics.EngineTab as EngineMetrics
@ -35,26 +34,11 @@ import org.mozilla.fenix.GleanMetrics.EngineTab as EngineMetrics
*/
class TelemetryMiddleware(
private val settings: Settings,
private val adsTelemetry: AdsTelemetry,
private val metrics: MetricController
) : Middleware<BrowserState, BrowserAction> {
private val logger = Logger("TelemetryMiddleware")
@VisibleForTesting
internal val redirectChains = mutableMapOf<String, RedirectChain>()
/**
* Utility to collect URLs / load requests in between location changes.
*/
internal class RedirectChain(internal val root: String) {
internal val chain = mutableListOf<String>()
fun add(url: String) {
chain.add(url)
}
}
@Suppress("TooGenericExceptionCaught", "ComplexMethod", "NestedBlockDepth")
override fun invoke(
context: MiddlewareContext<BrowserState, BrowserAction>,
@ -75,28 +59,6 @@ class TelemetryMiddleware(
}
}
}
is ContentAction.UpdateLoadRequestAction -> {
context.state.findTab(action.sessionId)?.let { tab ->
// Collect all load requests in between location changes
if (!redirectChains.containsKey(action.sessionId) && action.loadRequest.url != tab.content.url) {
redirectChains[action.sessionId] = RedirectChain(tab.content.url)
}
redirectChains[action.sessionId]?.add(action.loadRequest.url)
}
}
is ContentAction.UpdateUrlAction -> {
redirectChains[action.sessionId]?.let {
// Record ads telemetry providing all redirects
try {
adsTelemetry.trackAdClickedMetric(it.root, it.chain)
} catch (t: Throwable) {
logger.info("Failed to record search telemetry", t)
} finally {
redirectChains.remove(action.sessionId)
}
}
}
is DownloadAction.AddDownloadAction -> {
metrics.track(Event.DownloadAdded)
}

@ -1,41 +0,0 @@
/* 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
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
import org.junit.Test
import org.mozilla.fenix.search.telemetry.SearchProviderModel
class SearchProviderModelTest {
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.containsAdLinks(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.containsAdLinks(listOf(nonAd1, nonAd2)))
}
}

@ -1,75 +0,0 @@
package org.mozilla.fenix.search.telemetry
import io.mockk.mockk
import io.mockk.spyk
import io.mockk.verify
import mozilla.components.browser.state.store.BrowserStore
import mozilla.components.concept.engine.Engine
import org.json.JSONObject
import org.junit.Assert.assertEquals
import org.junit.Test
class BaseSearchTelemetryTest {
private lateinit var baseTelemetry: BaseSearchTelemetry
private lateinit var handler: BaseSearchTelemetry.SearchTelemetryMessageHandler
@org.junit.Before
fun setUp() {
baseTelemetry = spyk(object : BaseSearchTelemetry() {
override fun install(engine: Engine, store: BrowserStore) {
// mock, do nothing
}
override fun processMessage(message: JSONObject) {
// mock, do nothing
}
})
handler = baseTelemetry.SearchTelemetryMessageHandler()
}
@Test
fun install() {
val engine = mockk<Engine>(relaxed = true)
val store = mockk<BrowserStore>(relaxed = true)
val id = "id"
val resourceUrl = "resourceUrl"
val messageId = "messageId"
val extensionInfo = ExtensionInfo(id, resourceUrl, messageId)
baseTelemetry.installWebExtension(engine, store, extensionInfo)
verify {
engine.installWebExtension(
id = id,
url = resourceUrl,
onSuccess = any(),
onError = any()
)
}
}
@Test
fun `get provider for google url`() {
val url = "https://www.google.com/search?q=computers"
assertEquals("google", baseTelemetry.getProviderForUrl(url)?.name)
}
@Test
fun `message handler finds a valid json object`() {
val message = JSONObject()
handler.onMessage(message, mockk())
verify { baseTelemetry.processMessage(message) }
}
@Test(expected = IllegalStateException::class)
fun `message handler finds no json object`() {
val message = "message"
handler.onMessage(message, mockk())
}
}

@ -1,140 +0,0 @@
/* 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.ExtensionInfo
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_COOKIES_KEY
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_ID
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
@Before
fun setUp() {
ads = spyk(AdsTelemetry(metrics))
}
@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 sessionUrl = "https://www.google.com/search?q=aaa"
ads.trackAdClickedMetric(
sessionUrl,
listOf("https://www.google.com/aclk", "https://www.aaa.com")
)
verify { metrics.track(Event.SearchAdClicked("google.in-content.organic.none")) }
}
@Test
fun install() {
val engine = mockk<Engine>(relaxed = true)
val store = mockk<BrowserStore>(relaxed = true)
val extensionInfo = slot<ExtensionInfo>()
ads.install(engine, store)
verify { ads.installWebExtension(engine, store, capture(extensionInfo)) }
assertEquals(ADS_EXTENSION_ID, extensionInfo.captured.id)
assertEquals(ADS_EXTENSION_RESOURCE_URL, extensionInfo.captured.resourceUrl)
assertEquals(ADS_MESSAGE_ID, extensionInfo.captured.messageId)
}
@Test
fun `process 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 urls = JSONArray()
urls.put(first)
urls.put(second)
val cookies = JSONArray()
val message = JSONObject()
message.put(ADS_MESSAGE_DOCUMENT_URLS_KEY, urls)
message.put(ADS_MESSAGE_SESSION_URL_KEY, "https://www.google.com/search?q=aaa")
message.put(ADS_MESSAGE_COOKIES_KEY, cookies)
ads.processMessage(message)
verify { metrics.track(capture(metricEvent)) }
assertEquals(ads.providerList[0].name, metricEvent.captured.label)
}
@Test
fun `process the document urls and don't find ads`() {
val first = "https://www.google.com/aaaaaa"
val second = "https://www.google.com/aaa"
val urls = JSONArray()
urls.put(first)
urls.put(second)
val cookies = JSONArray()
val message = JSONObject()
message.put(ADS_MESSAGE_DOCUMENT_URLS_KEY, urls)
message.put(ADS_MESSAGE_SESSION_URL_KEY, "https://www.google.com/search?q=aaa")
message.put(ADS_MESSAGE_COOKIES_KEY, cookies)
ads.processMessage(message)
verify(exactly = 0) { metrics.track(any()) }
}
@Test
fun `track bing sap-follow-on metric by cookies`() {
val url = "https://www.bing.com/search?q=aaa&pc=MOZMBA&form=QBRERANDOM"
ads.cachedCookies = createCookieList()
ads.trackAdClickedMetric(url, listOf("https://www.bing.com/aclik", "https://www.aaa.com"))
verify { metrics.track(Event.SearchAdClicked("bing.in-content.sap-follow-on.mozmba")) }
}
private fun createCookieList(): List<JSONObject> {
val first = JSONObject()
first.put("name", "SRCHS")
first.put("value", "PC=MOZMBA")
val second = JSONObject()
second.put("name", "RANDOM")
second.put("value", "RANDOM")
return listOf(first, second)
}
}

@ -1,184 +0,0 @@
package org.mozilla.fenix.search.telemetry.incontent
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
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.ExtensionInfo
import org.mozilla.fenix.search.telemetry.incontent.InContentTelemetry.Companion.COOKIES_EXTENSION_ID
import org.mozilla.fenix.search.telemetry.incontent.InContentTelemetry.Companion.COOKIES_EXTENSION_RESOURCE_URL
import org.mozilla.fenix.search.telemetry.incontent.InContentTelemetry.Companion.COOKIES_MESSAGE_ID
import org.mozilla.fenix.search.telemetry.incontent.InContentTelemetry.Companion.COOKIES_MESSAGE_LIST_KEY
import org.mozilla.fenix.search.telemetry.incontent.InContentTelemetry.Companion.COOKIES_MESSAGE_SESSION_URL_KEY
@RunWith(FenixRobolectricTestRunner::class)
class InContentTelemetryTest {
private val metrics: MetricController = mockk(relaxed = true)
private lateinit var telemetry: InContentTelemetry
@Before
fun setUp() {
telemetry = spyk(InContentTelemetry(metrics))
}
@Test
fun install() {
val engine = mockk<Engine>(relaxed = true)
val store = mockk<BrowserStore>(relaxed = true)
val extensionInfo = slot<ExtensionInfo>()
telemetry.install(engine, store)
verify { telemetry.installWebExtension(engine, store, capture(extensionInfo)) }
Assert.assertEquals(COOKIES_EXTENSION_ID, extensionInfo.captured.id)
Assert.assertEquals(COOKIES_EXTENSION_RESOURCE_URL, extensionInfo.captured.resourceUrl)
Assert.assertEquals(COOKIES_MESSAGE_ID, extensionInfo.captured.messageId)
}
@Test
fun processMessage() {
val first = JSONObject()
val second = JSONObject()
val array = JSONArray()
array.put(first)
array.put(second)
val message = JSONObject()
val url = "https://www.google.com/search?q=aaa"
message.put(COOKIES_MESSAGE_LIST_KEY, array)
message.put(COOKIES_MESSAGE_SESSION_URL_KEY, url)
telemetry.processMessage(message)
verify { telemetry.trackPartnerUrlTypeMetric(url, listOf(first, second)) }
}
@Test
fun `track google sap metric`() {
val url = "https://www.google.com/search?q=aaa&client=firefox-b-m"
telemetry.trackPartnerUrlTypeMetric(url, listOf())
verify { metrics.track(Event.SearchInContent("google.in-content.sap.firefox-b-m")) }
}
@Test
fun `track duckduckgo sap metric`() {
val url = "https://duckduckgo.com/?q=aaa&t=fpas"
telemetry.trackPartnerUrlTypeMetric(url, listOf())
verify { metrics.track(Event.SearchInContent("duckduckgo.in-content.sap.fpas")) }
}
@Test
fun `track baidu sap metric`() {
val url = "https://m.baidu.com/s?from=1000969a&word=aaa"
telemetry.trackPartnerUrlTypeMetric(url, listOf())
verify { metrics.track(Event.SearchInContent("baidu.in-content.sap._1000969a")) }
}
@Test
fun `track bing sap metric`() {
val url = "https://www.bing.com/search?q=aaa&pc=MOZMBA"
telemetry.trackPartnerUrlTypeMetric(url, listOf())
verify { metrics.track(Event.SearchInContent("bing.in-content.sap.mozmba")) }
}
@Test
fun `track google sap-follow-on metric`() {
val url = "https://www.google.com/search?q=aaa&client=firefox-b-m&oq=random"
telemetry.trackPartnerUrlTypeMetric(url, listOf())
verify { metrics.track(Event.SearchInContent("google.in-content.sap-follow-on.firefox-b-m")) }
}
@Test
fun `track google sap-follow-on and topSite metric`() {
val url = "https://www.google.com/search?q=aaa&client=firefox-b-m&channel=ts&oq=random"
telemetry.trackPartnerUrlTypeMetric(url, listOf())
verify { metrics.track(Event.SearchInContent("google.in-content.sap-follow-on.firefox-b-m.ts")) }
}
@Test
fun `track baidu sap-follow-on metric`() {
val url = "https://m.baidu.com/s?from=1000969a&word=aaa&oq=random"
telemetry.trackPartnerUrlTypeMetric(url, listOf())
verify { metrics.track(Event.SearchInContent("baidu.in-content.sap-follow-on._1000969a")) }
}
@Test
fun `track bing sap-follow-on metric by cookies`() {
val url = "https://www.bing.com/search?q=aaa&pc=MOZMBA&form=QBRERANDOM"
telemetry.trackPartnerUrlTypeMetric(url, createCookieList())
verify { metrics.track(Event.SearchInContent("bing.in-content.sap-follow-on.mozmba")) }
}
@Test
fun `track google organic metric`() {
val url = "https://www.google.com/search?q=aaa"
telemetry.trackPartnerUrlTypeMetric(url, listOf())
verify { metrics.track(Event.SearchInContent("google.in-content.organic.none")) }
}
@Test
fun `track duckduckgo organic metric`() {
val url = "https://duckduckgo.com/?q=aaa"
telemetry.trackPartnerUrlTypeMetric(url, listOf())
verify { metrics.track(Event.SearchInContent("duckduckgo.in-content.organic.none")) }
}
@Test
fun `track bing organic metric`() {
val url = "https://www.bing.com/search?q=aaa"
telemetry.trackPartnerUrlTypeMetric(url, listOf())
verify { metrics.track(Event.SearchInContent("bing.in-content.organic.none")) }
}
@Test
fun `track baidu organic metric`() {
val url = "https://m.baidu.com/s?word=aaa"
telemetry.trackPartnerUrlTypeMetric(url, listOf())
verify { metrics.track(Event.SearchInContent("baidu.in-content.organic.none")) }
}
private fun createCookieList(): List<JSONObject> {
val first = JSONObject()
first.put("name", "SRCHS")
first.put("value", "PC=MOZMBA")
val second = JSONObject()
second.put("name", "RANDOM")
second.put("value", "RANDOM")
return listOf(first, second)
}
}

@ -15,7 +15,6 @@ import mozilla.components.browser.state.action.DownloadAction
import mozilla.components.browser.state.action.EngineAction
import mozilla.components.browser.state.action.TabListAction
import mozilla.components.browser.state.state.BrowserState
import mozilla.components.browser.state.state.LoadRequestState
import mozilla.components.browser.state.state.createTab
import mozilla.components.browser.state.store.BrowserStore
import mozilla.components.service.glean.testing.GleanTestRule
@ -27,8 +26,6 @@ import mozilla.components.support.test.rule.MainCoroutineRule
import org.junit.After
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertNotNull
import org.junit.Assert.assertNull
import org.junit.Assert.assertTrue
import org.junit.Before
import org.junit.Rule
@ -37,7 +34,6 @@ 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
import org.mozilla.fenix.utils.Settings
import org.mozilla.fenix.GleanMetrics.EngineTab as EngineMetrics
@ -49,7 +45,6 @@ class TelemetryMiddlewareTest {
private lateinit var settings: Settings
private lateinit var telemetryMiddleware: TelemetryMiddleware
private lateinit var metrics: MetricController
private lateinit var adsTelemetry: AdsTelemetry
private val testDispatcher = TestCoroutineDispatcher()
@get:Rule
@ -66,10 +61,8 @@ class TelemetryMiddlewareTest {
settings = Settings(testContext)
metrics = mockk(relaxed = true)
adsTelemetry = mockk()
telemetryMiddleware = TelemetryMiddleware(
settings,
adsTelemetry,
metrics
)
store = BrowserStore(
@ -203,70 +196,6 @@ class TelemetryMiddlewareTest {
verify(exactly = 1) { metrics.track(Event.NormalAndPrivateUriOpened) }
}
@Test
fun `GIVEN a load request WHEN no redirect chain is available THEN a new chain will be created`() {
val tab = createTab(id = "1", url = "http://mozilla.org")
store.dispatch(TabListAction.AddTabAction(tab)).joinBlocking()
store.dispatch(ContentAction.UpdateLoadRequestAction(
tab.id, LoadRequestState(tab.content.url, true, true))
).joinBlocking()
assertNull(telemetryMiddleware.redirectChains[tab.id])
store.dispatch(ContentAction.UpdateLoadRequestAction(
tab.id, LoadRequestState("https://mozilla.org", true, true))
).joinBlocking()
assertNotNull(telemetryMiddleware.redirectChains[tab.id])
assertEquals(tab.content.url, telemetryMiddleware.redirectChains[tab.id]!!.root)
}
@Test
fun `GIVEN a load request WHEN a redirect chain is available THEN url is added to chain`() {
val tab = createTab(id = "1", url = "http://mozilla.org")
store.dispatch(TabListAction.AddTabAction(tab)).joinBlocking()
store.dispatch(ContentAction.UpdateLoadRequestAction(
tab.id, LoadRequestState("https://mozilla.org", true, true))
).joinBlocking()
assertNotNull(telemetryMiddleware.redirectChains[tab.id])
assertEquals(tab.content.url, telemetryMiddleware.redirectChains[tab.id]!!.root)
assertEquals("https://mozilla.org", telemetryMiddleware.redirectChains[tab.id]!!.chain.first())
}
@Test
fun `GIVEN a location update WHEN no redirect chain is available THEN no ads telemetry is recorded`() {
val tab = createTab(id = "1", url = "http://mozilla.org")
store.dispatch(ContentAction.UpdateUrlAction(tab.id, "http://mozilla.org")).joinBlocking()
verify(exactly = 0) { adsTelemetry.trackAdClickedMetric(any(), any()) }
}
@Test
fun `GIVEN a location update WHEN a redirect chain is available THEN ads telemetry is recorded`() {
val tab = createTab(id = "1", url = "http://mozilla.org")
store.dispatch(TabListAction.AddTabAction(tab)).joinBlocking()
store.dispatch(ContentAction.UpdateLoadRequestAction(
tab.id, LoadRequestState("https://mozilla.org", true, true))
).joinBlocking()
store.dispatch(ContentAction.UpdateUrlAction(tab.id, "https://mozilla.org")).joinBlocking()
verify(exactly = 1) { adsTelemetry.trackAdClickedMetric(tab.content.url, listOf("https://mozilla.org")) }
}
@Test
fun `GIVEN a location update WHEN ads telemetry is recorded THEN redirect chain is reset`() {
val tab = createTab(id = "1", url = "http://mozilla.org")
store.dispatch(TabListAction.AddTabAction(tab)).joinBlocking()
store.dispatch(ContentAction.UpdateLoadRequestAction(
tab.id, LoadRequestState("https://mozilla.org", true, true))
).joinBlocking()
assertNotNull(telemetryMiddleware.redirectChains[tab.id])
store.dispatch(ContentAction.UpdateUrlAction(tab.id, "https://mozilla.org")).joinBlocking()
assertNull(telemetryMiddleware.redirectChains[tab.id])
}
@Test
fun `WHEN a download is added THEN the downloads count is updated`() {
store.dispatch(DownloadAction.AddDownloadAction(mock())).joinBlocking()

@ -3,5 +3,5 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
object AndroidComponents {
const val VERSION = "91.0.20210712190108"
const val VERSION = "92.0.20210714133214"
}

Loading…
Cancel
Save