From 5b7412c8dea29f62a71c924e193174bb17239cd6 Mon Sep 17 00:00:00 2001 From: Mugurell Date: Fri, 2 Jul 2021 09:07:47 +0300 Subject: [PATCH] [components] For https://github.com/mozilla-mobile/android-components/issues/7249 - New ads / search telemetry This is an upstream of the functionality already existing on Fenix so that it can be used by multiple AC clients. AdsTelemetry can identify whether there are ads shown in search results in which case a (Component.FEATURE_SEARCH to SERP_SHOWN_WITH_ADDS) Fact will be emitted. If AdsMiddleware is installed it will also track when an ad was clicked for which a (Component.FEATURE_SEARCH to SERP_ADD_CLICKED) Fact will be emitted. InContentTelemetry can identify follow-on and organic web searches for which a (Component.FEATURE_SEARCH to IN_CONTENT_SEARCH) Fact will be emitted. In both cases the functionality only works for specific providers declared in BaseSearchTelemetry and will be limited to only crawl websites matching the ones declared in web extensions' manifest.json. As such these lists will have to be permanently kept up-to-date. --- .../components/feature/search/.gitignore | 1 + .../components/feature/search/build.gradle | 12 + .../src/main/assets/extensions/ads/adsTelemetry.js | 82 ++++ .../assets/extensions/ads/manifest.template.json | 413 ++++++++++++++++++++ .../extensions/search/manifest.template.json | 414 +++++++++++++++++++++ .../assets/extensions/search/searchTelemetry.js | 61 +++ .../search/middleware/AdsTelemetryMiddleware.kt | 77 ++++ .../search/telemetry/BaseSearchTelemetry.kt | 174 +++++++++ .../feature/search/telemetry/ExtensionInfo.kt | 18 + .../search/telemetry/SearchProviderCookie.kt | 26 ++ .../search/telemetry/SearchProviderModel.kt | 59 +++ .../feature/search/telemetry/TrackKeyInfo.kt | 36 ++ .../components/feature/search/telemetry/Utils.kt | 115 ++++++ .../feature/search/telemetry/ads/AdsTelemetry.kt | 119 ++++++ .../telemetry/incontent/InContentTelemetry.kt | 78 ++++ .../middleware/AdsTelemetryMiddlewareTest.kt | 111 ++++++ .../search/telemetry/BaseSearchTelemetryTest.kt | 87 +++++ .../search/telemetry/SearchProviderModelTest.kt | 38 ++ .../search/telemetry/ads/AdsTelemetryTest.kt | 190 ++++++++++ .../telemetry/incontent/InContentTelemetryTest.kt | 310 +++++++++++++++ .../android/android-components/docs/changelog.md | 4 + 21 files changed, 2425 insertions(+) create mode 100644 mobile/android/android-components/components/feature/search/.gitignore create mode 100644 mobile/android/android-components/components/feature/search/src/main/assets/extensions/ads/adsTelemetry.js create mode 100644 mobile/android/android-components/components/feature/search/src/main/assets/extensions/ads/manifest.template.json create mode 100644 mobile/android/android-components/components/feature/search/src/main/assets/extensions/search/manifest.template.json create mode 100644 mobile/android/android-components/components/feature/search/src/main/assets/extensions/search/searchTelemetry.js create mode 100644 mobile/android/android-components/components/feature/search/src/main/java/mozilla/components/feature/search/middleware/AdsTelemetryMiddleware.kt create mode 100644 mobile/android/android-components/components/feature/search/src/main/java/mozilla/components/feature/search/telemetry/BaseSearchTelemetry.kt create mode 100644 mobile/android/android-components/components/feature/search/src/main/java/mozilla/components/feature/search/telemetry/ExtensionInfo.kt create mode 100644 mobile/android/android-components/components/feature/search/src/main/java/mozilla/components/feature/search/telemetry/SearchProviderCookie.kt create mode 100644 mobile/android/android-components/components/feature/search/src/main/java/mozilla/components/feature/search/telemetry/SearchProviderModel.kt create mode 100644 mobile/android/android-components/components/feature/search/src/main/java/mozilla/components/feature/search/telemetry/TrackKeyInfo.kt create mode 100644 mobile/android/android-components/components/feature/search/src/main/java/mozilla/components/feature/search/telemetry/Utils.kt create mode 100644 mobile/android/android-components/components/feature/search/src/main/java/mozilla/components/feature/search/telemetry/ads/AdsTelemetry.kt create mode 100644 mobile/android/android-components/components/feature/search/src/main/java/mozilla/components/feature/search/telemetry/incontent/InContentTelemetry.kt create mode 100644 mobile/android/android-components/components/feature/search/src/test/java/mozilla/components/feature/search/middleware/AdsTelemetryMiddlewareTest.kt create mode 100644 mobile/android/android-components/components/feature/search/src/test/java/mozilla/components/feature/search/telemetry/BaseSearchTelemetryTest.kt create mode 100644 mobile/android/android-components/components/feature/search/src/test/java/mozilla/components/feature/search/telemetry/SearchProviderModelTest.kt create mode 100644 mobile/android/android-components/components/feature/search/src/test/java/mozilla/components/feature/search/telemetry/ads/AdsTelemetryTest.kt create mode 100644 mobile/android/android-components/components/feature/search/src/test/java/mozilla/components/feature/search/telemetry/incontent/InContentTelemetryTest.kt diff --git a/mobile/android/android-components/components/feature/search/.gitignore b/mobile/android/android-components/components/feature/search/.gitignore new file mode 100644 index 000000000000..2ddf5f27b162 --- /dev/null +++ b/mobile/android/android-components/components/feature/search/.gitignore @@ -0,0 +1 @@ +manifest.json diff --git a/mobile/android/android-components/components/feature/search/build.gradle b/mobile/android/android-components/components/feature/search/build.gradle index a45c446a8bcd..4b95be2312a3 100644 --- a/mobile/android/android-components/components/feature/search/build.gradle +++ b/mobile/android/android-components/components/feature/search/build.gradle @@ -57,5 +57,17 @@ dependencies { testImplementation Dependencies.testing_coroutines } + +tasks.register("updateAdsExtensionVersion", org.gradle.api.tasks.Copy) { task -> + updateExtensionVersion(task, 'src/main/assets/extensions/ads') +} + +tasks.register("updateCookiesExtensionVersion", org.gradle.api.tasks.Copy) { task -> + updateExtensionVersion(task, 'src/main/assets/extensions/search') +} + +preBuild.dependsOn "updateAdsExtensionVersion" +preBuild.dependsOn "updateCookiesExtensionVersion" + apply from: '../../../publish.gradle' ext.configurePublish(config.componentsGroupId, archivesBaseName, project.ext.description) diff --git a/mobile/android/android-components/components/feature/search/src/main/assets/extensions/ads/adsTelemetry.js b/mobile/android/android-components/components/feature/search/src/main/assets/extensions/ads/adsTelemetry.js new file mode 100644 index 000000000000..65bf3068351b --- /dev/null +++ b/mobile/android/android-components/components/feature/search/src/main/assets/extensions/ads/adsTelemetry.js @@ -0,0 +1,82 @@ +/* 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/. */ + +/** + * Send + * - current URL + * - cookies of this page + * - all links found in this page + * to the native application. + */ +function sendCurrentState() { + let message = { + 'url': document.location.href, + 'urls': getLinks(), + 'cookies': getCookies() + }; + browser.runtime.sendNativeMessage("MozacBrowserAdsMessage", message); +} + +/** + * Get all links in the current page. + * + * @return {Array} containing all current links in the current page. + */ +function getLinks() { + let urls = []; + + let anchors = document.getElementsByTagName("a"); + for (let anchor of anchors) { + if (!anchor.href) { + continue; + } + urls.push(anchor.href); + } + + return urls; +} + +/** + * Get all cookies for the current document. + * + * @return {Array<{name: string, value: string}>} containing all cookies. + */ +function getCookies() { + let cookiesList = document.cookie.split("; "); + let result = []; + + cookiesList.forEach(cookie => { + var [name, ...value] = cookie.split('='); + // For that special cases where the value contains '='. + value = value.join("=") + + result.push({ + "name" : name, + "value" : value + }); + }); + + return result; +} + +// Whenever a page is first accessed or when loaded from cache +// send all needed data about the ads provider to the app. +const events = ["pageshow", "load"]; +const eventLogger = event => { + switch (event.type) { + case "load": + sendCurrentState(); + break; + case "pageshow": + if (event.persisted) { + sendCurrentState(); + } + break; + default: + console.log('Event:', event.type); + } +}; +events.forEach(eventName => + window.addEventListener(eventName, eventLogger) +); diff --git a/mobile/android/android-components/components/feature/search/src/main/assets/extensions/ads/manifest.template.json b/mobile/android/android-components/components/feature/search/src/main/assets/extensions/ads/manifest.template.json new file mode 100644 index 000000000000..03835f26fd02 --- /dev/null +++ b/mobile/android/android-components/components/feature/search/src/main/assets/extensions/ads/manifest.template.json @@ -0,0 +1,413 @@ +{ + "manifest_version": 2, + "applications": { + "gecko": { + "id": "ads@mozac.org" + } + }, + "name": "Mozilla Android Components - Ads Telemetry", + "version": "${version}", + "content_scripts": [ + { + "matches": ["https://*/*"], + "include_globs": [ + "https://www.google.com/search*", + "https://www.google.ad/search*", + "https://www.google.ae/search*", + "https://www.google.com.af/search*", + "https://www.google.com.ag/search*", + "https://www.google.com.ai/search*", + "https://www.google.al/search*", + "https://www.google.am/search*", + "https://www.google.co.ao/search*", + "https://www.google.com.ar/search*", + "https://www.google.as/search*", + "https://www.google.at/search*", + "https://www.google.com.au/search*", + "https://www.google.az/search*", + "https://www.google.ba/search*", + "https://www.google.com.bd/search*", + "https://www.google.be/search*", + "https://www.google.bf/search*", + "https://www.google.bg/search*", + "https://www.google.com.bh/search*", + "https://www.google.bi/search*", + "https://www.google.bj/search*", + "https://www.google.com.bn/search*", + "https://www.google.com.bo/search*", + "https://www.google.com.br/search*", + "https://www.google.bs/search*", + "https://www.google.bt/search*", + "https://www.google.co.bw/search*", + "https://www.google.by/search*", + "https://www.google.com.bz/search*", + "https://www.google.ca/search*", + "https://www.google.cd/search*", + "https://www.google.cf/search*", + "https://www.google.cg/search*", + "https://www.google.ch/search*", + "https://www.google.ci/search*", + "https://www.google.co.ck/search*", + "https://www.google.cl/search*", + "https://www.google.cm/search*", + "https://www.google.cn/search*", + "https://www.google.com.co/search*", + "https://www.google.co.cr/search*", + "https://www.google.com.cu/search*", + "https://www.google.cv/search*", + "https://www.google.com.cy/search*", + "https://www.google.cz/search*", + "https://www.google.de/search*", + "https://www.google.dj/search*", + "https://www.google.dk/search*", + "https://www.google.dm/search*", + "https://www.google.com.do/search*", + "https://www.google.dz/search*", + "https://www.google.com.ec/search*", + "https://www.google.ee/search*", + "https://www.google.com.eg/search*", + "https://www.google.es/search*", + "https://www.google.com.et/search*", + "https://www.google.fi/search*", + "https://www.google.com.fj/search*", + "https://www.google.fm/search*", + "https://www.google.fr/search*", + "https://www.google.ga/search*", + "https://www.google.ge/search*", + "https://www.google.gg/search*", + "https://www.google.com.gh/search*", + "https://www.google.com.gi/search*", + "https://www.google.gl/search*", + "https://www.google.gm/search*", + "https://www.google.gr/search*", + "https://www.google.com.gt/search*", + "https://www.google.gy/search*", + "https://www.google.com.hk/search*", + "https://www.google.hn/search*", + "https://www.google.hr/search*", + "https://www.google.ht/search*", + "https://www.google.hu/search*", + "https://www.google.co.id/search*", + "https://www.google.ie/search*", + "https://www.google.co.il/search*", + "https://www.google.im/search*", + "https://www.google.co.in/search*", + "https://www.google.iq/search*", + "https://www.google.is/search*", + "https://www.google.it/search*", + "https://www.google.je/search*", + "https://www.google.com.jm/search*", + "https://www.google.jo/search*", + "https://www.google.co.jp/search*", + "https://www.google.co.ke/search*", + "https://www.google.com.kh/search*", + "https://www.google.ki/search*", + "https://www.google.kg/search*", + "https://www.google.co.kr/search*", + "https://www.google.com.kw/search*", + "https://www.google.kz/search*", + "https://www.google.la/search*", + "https://www.google.com.lb/search*", + "https://www.google.li/search*", + "https://www.google.lk/search*", + "https://www.google.co.ls/search*", + "https://www.google.lt/search*", + "https://www.google.lu/search*", + "https://www.google.lv/search*", + "https://www.google.com.ly/search*", + "https://www.google.co.ma/search*", + "https://www.google.md/search*", + "https://www.google.me/search*", + "https://www.google.mg/search*", + "https://www.google.mk/search*", + "https://www.google.ml/search*", + "https://www.google.com.mm/search*", + "https://www.google.mn/search*", + "https://www.google.ms/search*", + "https://www.google.com.mt/search*", + "https://www.google.mu/search*", + "https://www.google.mv/search*", + "https://www.google.mw/search*", + "https://www.google.com.mx/search*", + "https://www.google.com.my/search*", + "https://www.google.co.mz/search*", + "https://www.google.com.na/search*", + "https://www.google.com.ng/search*", + "https://www.google.com.ni/search*", + "https://www.google.ne/search*", + "https://www.google.nl/search*", + "https://www.google.no/search*", + "https://www.google.com.np/search*", + "https://www.google.nr/search*", + "https://www.google.nu/search*", + "https://www.google.co.nz/search*", + "https://www.google.com.om/search*", + "https://www.google.com.pa/search*", + "https://www.google.com.pe/search*", + "https://www.google.com.pg/search*", + "https://www.google.com.ph/search*", + "https://www.google.com.pk/search*", + "https://www.google.pl/search*", + "https://www.google.pn/search*", + "https://www.google.com.pr/search*", + "https://www.google.ps/search*", + "https://www.google.pt/search*", + "https://www.google.com.py/search*", + "https://www.google.com.qa/search*", + "https://www.google.ro/search*", + "https://www.google.ru/search*", + "https://www.google.rw/search*", + "https://www.google.com.sa/search*", + "https://www.google.com.sb/search*", + "https://www.google.sc/search*", + "https://www.google.se/search*", + "https://www.google.com.sg/search*", + "https://www.google.sh/search*", + "https://www.google.si/search*", + "https://www.google.sk/search*", + "https://www.google.com.sl/search*", + "https://www.google.sn/search*", + "https://www.google.so/search*", + "https://www.google.sm/search*", + "https://www.google.sr/search*", + "https://www.google.st/search*", + "https://www.google.com.sv/search*", + "https://www.google.td/search*", + "https://www.google.tg/search*", + "https://www.google.co.th/search*", + "https://www.google.com.tj/search*", + "https://www.google.tl/search*", + "https://www.google.tm/search*", + "https://www.google.tn/search*", + "https://www.google.to/search*", + "https://www.google.com.tr/search*", + "https://www.google.tt/search*", + "https://www.google.com.tw/search*", + "https://www.google.co.tz/search*", + "https://www.google.com.ua/search*", + "https://www.google.co.ug/search*", + "https://www.google.co.uk/search*", + "https://www.google.com.uy/search*", + "https://www.google.co.uz/search*", + "https://www.google.com.vc/search*", + "https://www.google.co.ve/search*", + "https://www.google.vg/search*", + "https://www.google.co.vi/search*", + "https://www.google.com.vn/search*", + "https://www.google.vu/search*", + "https://www.google.ws/search*", + "https://www.google.rs/search*", + "https://www.google.co.za/search*", + "https://www.google.co.zm/search*", + "https://www.google.co.zw/search*", + "https://www.google.cat/search*", + "https://www.bing.com/search*", + "https://www.baidu.com/*", + "https://m.baidu.com/*", + "https://duckduckgo.com/*" + ], + "js": ["adsTelemetry.js"], + "run_at": "document_end" + } + ], + "permissions": [ + "geckoViewAddons", + "nativeMessaging", + "nativeMessagingFromContent", + "https://www.google.com/search*", + "https://www.google.ad/search*", + "https://www.google.ae/search*", + "https://www.google.com.af/search*", + "https://www.google.com.ag/search*", + "https://www.google.com.ai/search*", + "https://www.google.al/search*", + "https://www.google.am/search*", + "https://www.google.co.ao/search*", + "https://www.google.com.ar/search*", + "https://www.google.as/search*", + "https://www.google.at/search*", + "https://www.google.com.au/search*", + "https://www.google.az/search*", + "https://www.google.ba/search*", + "https://www.google.com.bd/search*", + "https://www.google.be/search*", + "https://www.google.bf/search*", + "https://www.google.bg/search*", + "https://www.google.com.bh/search*", + "https://www.google.bi/search*", + "https://www.google.bj/search*", + "https://www.google.com.bn/search*", + "https://www.google.com.bo/search*", + "https://www.google.com.br/search*", + "https://www.google.bs/search*", + "https://www.google.bt/search*", + "https://www.google.co.bw/search*", + "https://www.google.by/search*", + "https://www.google.com.bz/search*", + "https://www.google.ca/search*", + "https://www.google.cd/search*", + "https://www.google.cf/search*", + "https://www.google.cg/search*", + "https://www.google.ch/search*", + "https://www.google.ci/search*", + "https://www.google.co.ck/search*", + "https://www.google.cl/search*", + "https://www.google.cm/search*", + "https://www.google.cn/search*", + "https://www.google.com.co/search*", + "https://www.google.co.cr/search*", + "https://www.google.com.cu/search*", + "https://www.google.cv/search*", + "https://www.google.com.cy/search*", + "https://www.google.cz/search*", + "https://www.google.de/search*", + "https://www.google.dj/search*", + "https://www.google.dk/search*", + "https://www.google.dm/search*", + "https://www.google.com.do/search*", + "https://www.google.dz/search*", + "https://www.google.com.ec/search*", + "https://www.google.ee/search*", + "https://www.google.com.eg/search*", + "https://www.google.es/search*", + "https://www.google.com.et/search*", + "https://www.google.fi/search*", + "https://www.google.com.fj/search*", + "https://www.google.fm/search*", + "https://www.google.fr/search*", + "https://www.google.ga/search*", + "https://www.google.ge/search*", + "https://www.google.gg/search*", + "https://www.google.com.gh/search*", + "https://www.google.com.gi/search*", + "https://www.google.gl/search*", + "https://www.google.gm/search*", + "https://www.google.gr/search*", + "https://www.google.com.gt/search*", + "https://www.google.gy/search*", + "https://www.google.com.hk/search*", + "https://www.google.hn/search*", + "https://www.google.hr/search*", + "https://www.google.ht/search*", + "https://www.google.hu/search*", + "https://www.google.co.id/search*", + "https://www.google.ie/search*", + "https://www.google.co.il/search*", + "https://www.google.im/search*", + "https://www.google.co.in/search*", + "https://www.google.iq/search*", + "https://www.google.is/search*", + "https://www.google.it/search*", + "https://www.google.je/search*", + "https://www.google.com.jm/search*", + "https://www.google.jo/search*", + "https://www.google.co.jp/search*", + "https://www.google.co.ke/search*", + "https://www.google.com.kh/search*", + "https://www.google.ki/search*", + "https://www.google.kg/search*", + "https://www.google.co.kr/search*", + "https://www.google.com.kw/search*", + "https://www.google.kz/search*", + "https://www.google.la/search*", + "https://www.google.com.lb/search*", + "https://www.google.li/search*", + "https://www.google.lk/search*", + "https://www.google.co.ls/search*", + "https://www.google.lt/search*", + "https://www.google.lu/search*", + "https://www.google.lv/search*", + "https://www.google.com.ly/search*", + "https://www.google.co.ma/search*", + "https://www.google.md/search*", + "https://www.google.me/search*", + "https://www.google.mg/search*", + "https://www.google.mk/search*", + "https://www.google.ml/search*", + "https://www.google.com.mm/search*", + "https://www.google.mn/search*", + "https://www.google.ms/search*", + "https://www.google.com.mt/search*", + "https://www.google.mu/search*", + "https://www.google.mv/search*", + "https://www.google.mw/search*", + "https://www.google.com.mx/search*", + "https://www.google.com.my/search*", + "https://www.google.co.mz/search*", + "https://www.google.com.na/search*", + "https://www.google.com.ng/search*", + "https://www.google.com.ni/search*", + "https://www.google.ne/search*", + "https://www.google.nl/search*", + "https://www.google.no/search*", + "https://www.google.com.np/search*", + "https://www.google.nr/search*", + "https://www.google.nu/search*", + "https://www.google.co.nz/search*", + "https://www.google.com.om/search*", + "https://www.google.com.pa/search*", + "https://www.google.com.pe/search*", + "https://www.google.com.pg/search*", + "https://www.google.com.ph/search*", + "https://www.google.com.pk/search*", + "https://www.google.pl/search*", + "https://www.google.pn/search*", + "https://www.google.com.pr/search*", + "https://www.google.ps/search*", + "https://www.google.pt/search*", + "https://www.google.com.py/search*", + "https://www.google.com.qa/search*", + "https://www.google.ro/search*", + "https://www.google.ru/search*", + "https://www.google.rw/search*", + "https://www.google.com.sa/search*", + "https://www.google.com.sb/search*", + "https://www.google.sc/search*", + "https://www.google.se/search*", + "https://www.google.com.sg/search*", + "https://www.google.sh/search*", + "https://www.google.si/search*", + "https://www.google.sk/search*", + "https://www.google.com.sl/search*", + "https://www.google.sn/search*", + "https://www.google.so/search*", + "https://www.google.sm/search*", + "https://www.google.sr/search*", + "https://www.google.st/search*", + "https://www.google.com.sv/search*", + "https://www.google.td/search*", + "https://www.google.tg/search*", + "https://www.google.co.th/search*", + "https://www.google.com.tj/search*", + "https://www.google.tl/search*", + "https://www.google.tm/search*", + "https://www.google.tn/search*", + "https://www.google.to/search*", + "https://www.google.com.tr/search*", + "https://www.google.tt/search*", + "https://www.google.com.tw/search*", + "https://www.google.co.tz/search*", + "https://www.google.com.ua/search*", + "https://www.google.co.ug/search*", + "https://www.google.co.uk/search*", + "https://www.google.com.uy/search*", + "https://www.google.co.uz/search*", + "https://www.google.com.vc/search*", + "https://www.google.co.ve/search*", + "https://www.google.vg/search*", + "https://www.google.co.vi/search*", + "https://www.google.com.vn/search*", + "https://www.google.vu/search*", + "https://www.google.ws/search*", + "https://www.google.rs/search*", + "https://www.google.co.za/search*", + "https://www.google.co.zm/search*", + "https://www.google.co.zw/search*", + "https://www.google.cat/search*", + "https://www.baidu.com/*", + "https://m.baidu.com/*", + "https://*search.yahoo.com/search*", + "https://www.bing.com/search*", + "https://duckduckgo.com/*" + ] +} diff --git a/mobile/android/android-components/components/feature/search/src/main/assets/extensions/search/manifest.template.json b/mobile/android/android-components/components/feature/search/src/main/assets/extensions/search/manifest.template.json new file mode 100644 index 000000000000..d7b9b1a8405b --- /dev/null +++ b/mobile/android/android-components/components/feature/search/src/main/assets/extensions/search/manifest.template.json @@ -0,0 +1,414 @@ +{ + "manifest_version": 2, + "applications": { + "gecko": { + "id": "cookies@mozac.org" + } + }, + "name": "Mozilla Android Components - Search Telemetry", + "version": "${version}", + "content_scripts": [ + { + "matches": ["https://*/*"], + "include_globs": [ + "https://www.google.com/search*", + "https://www.google.ad/search*", + "https://www.google.ae/search*", + "https://www.google.com.af/search*", + "https://www.google.com.ag/search*", + "https://www.google.com.ai/search*", + "https://www.google.al/search*", + "https://www.google.am/search*", + "https://www.google.co.ao/search*", + "https://www.google.com.ar/search*", + "https://www.google.as/search*", + "https://www.google.at/search*", + "https://www.google.com.au/search*", + "https://www.google.az/search*", + "https://www.google.ba/search*", + "https://www.google.com.bd/search*", + "https://www.google.be/search*", + "https://www.google.bf/search*", + "https://www.google.bg/search*", + "https://www.google.com.bh/search*", + "https://www.google.bi/search*", + "https://www.google.bj/search*", + "https://www.google.com.bn/search*", + "https://www.google.com.bo/search*", + "https://www.google.com.br/search*", + "https://www.google.bs/search*", + "https://www.google.bt/search*", + "https://www.google.co.bw/search*", + "https://www.google.by/search*", + "https://www.google.com.bz/search*", + "https://www.google.ca/search*", + "https://www.google.cd/search*", + "https://www.google.cf/search*", + "https://www.google.cg/search*", + "https://www.google.ch/search*", + "https://www.google.ci/search*", + "https://www.google.co.ck/search*", + "https://www.google.cl/search*", + "https://www.google.cm/search*", + "https://www.google.cn/search*", + "https://www.google.com.co/search*", + "https://www.google.co.cr/search*", + "https://www.google.com.cu/search*", + "https://www.google.cv/search*", + "https://www.google.com.cy/search*", + "https://www.google.cz/search*", + "https://www.google.de/search*", + "https://www.google.dj/search*", + "https://www.google.dk/search*", + "https://www.google.dm/search*", + "https://www.google.com.do/search*", + "https://www.google.dz/search*", + "https://www.google.com.ec/search*", + "https://www.google.ee/search*", + "https://www.google.com.eg/search*", + "https://www.google.es/search*", + "https://www.google.com.et/search*", + "https://www.google.fi/search*", + "https://www.google.com.fj/search*", + "https://www.google.fm/search*", + "https://www.google.fr/search*", + "https://www.google.ga/search*", + "https://www.google.ge/search*", + "https://www.google.gg/search*", + "https://www.google.com.gh/search*", + "https://www.google.com.gi/search*", + "https://www.google.gl/search*", + "https://www.google.gm/search*", + "https://www.google.gr/search*", + "https://www.google.com.gt/search*", + "https://www.google.gy/search*", + "https://www.google.com.hk/search*", + "https://www.google.hn/search*", + "https://www.google.hr/search*", + "https://www.google.ht/search*", + "https://www.google.hu/search*", + "https://www.google.co.id/search*", + "https://www.google.ie/search*", + "https://www.google.co.il/search*", + "https://www.google.im/search*", + "https://www.google.co.in/search*", + "https://www.google.iq/search*", + "https://www.google.is/search*", + "https://www.google.it/search*", + "https://www.google.je/search*", + "https://www.google.com.jm/search*", + "https://www.google.jo/search*", + "https://www.google.co.jp/search*", + "https://www.google.co.ke/search*", + "https://www.google.com.kh/search*", + "https://www.google.ki/search*", + "https://www.google.kg/search*", + "https://www.google.co.kr/search*", + "https://www.google.com.kw/search*", + "https://www.google.kz/search*", + "https://www.google.la/search*", + "https://www.google.com.lb/search*", + "https://www.google.li/search*", + "https://www.google.lk/search*", + "https://www.google.co.ls/search*", + "https://www.google.lt/search*", + "https://www.google.lu/search*", + "https://www.google.lv/search*", + "https://www.google.com.ly/search*", + "https://www.google.co.ma/search*", + "https://www.google.md/search*", + "https://www.google.me/search*", + "https://www.google.mg/search*", + "https://www.google.mk/search*", + "https://www.google.ml/search*", + "https://www.google.com.mm/search*", + "https://www.google.mn/search*", + "https://www.google.ms/search*", + "https://www.google.com.mt/search*", + "https://www.google.mu/search*", + "https://www.google.mv/search*", + "https://www.google.mw/search*", + "https://www.google.com.mx/search*", + "https://www.google.com.my/search*", + "https://www.google.co.mz/search*", + "https://www.google.com.na/search*", + "https://www.google.com.ng/search*", + "https://www.google.com.ni/search*", + "https://www.google.ne/search*", + "https://www.google.nl/search*", + "https://www.google.no/search*", + "https://www.google.com.np/search*", + "https://www.google.nr/search*", + "https://www.google.nu/search*", + "https://www.google.co.nz/search*", + "https://www.google.com.om/search*", + "https://www.google.com.pa/search*", + "https://www.google.com.pe/search*", + "https://www.google.com.pg/search*", + "https://www.google.com.ph/search*", + "https://www.google.com.pk/search*", + "https://www.google.pl/search*", + "https://www.google.pn/search*", + "https://www.google.com.pr/search*", + "https://www.google.ps/search*", + "https://www.google.pt/search*", + "https://www.google.com.py/search*", + "https://www.google.com.qa/search*", + "https://www.google.ro/search*", + "https://www.google.ru/search*", + "https://www.google.rw/search*", + "https://www.google.com.sa/search*", + "https://www.google.com.sb/search*", + "https://www.google.sc/search*", + "https://www.google.se/search*", + "https://www.google.com.sg/search*", + "https://www.google.sh/search*", + "https://www.google.si/search*", + "https://www.google.sk/search*", + "https://www.google.com.sl/search*", + "https://www.google.sn/search*", + "https://www.google.so/search*", + "https://www.google.sm/search*", + "https://www.google.sr/search*", + "https://www.google.st/search*", + "https://www.google.com.sv/search*", + "https://www.google.td/search*", + "https://www.google.tg/search*", + "https://www.google.co.th/search*", + "https://www.google.com.tj/search*", + "https://www.google.tl/search*", + "https://www.google.tm/search*", + "https://www.google.tn/search*", + "https://www.google.to/search*", + "https://www.google.com.tr/search*", + "https://www.google.tt/search*", + "https://www.google.com.tw/search*", + "https://www.google.co.tz/search*", + "https://www.google.com.ua/search*", + "https://www.google.co.ug/search*", + "https://www.google.co.uk/search*", + "https://www.google.com.uy/search*", + "https://www.google.co.uz/search*", + "https://www.google.com.vc/search*", + "https://www.google.co.ve/search*", + "https://www.google.vg/search*", + "https://www.google.co.vi/search*", + "https://www.google.com.vn/search*", + "https://www.google.vu/search*", + "https://www.google.ws/search*", + "https://www.google.rs/search*", + "https://www.google.co.za/search*", + "https://www.google.co.zm/search*", + "https://www.google.co.zw/search*", + "https://www.google.cat/search*", + "https://www.baidu.com/*", + "https://m.baidu.com/*", + "https://*search.yahoo.com/search*", + "https://www.bing.com/search*", + "https://duckduckgo.com/*" + ], + "js": ["searchTelemetry.js"], + "run_at": "document_end" + } + ], + "permissions": [ + "geckoViewAddons", + "nativeMessaging", + "nativeMessagingFromContent", + "https://www.google.com/search*", + "https://www.google.ad/search*", + "https://www.google.ae/search*", + "https://www.google.com.af/search*", + "https://www.google.com.ag/search*", + "https://www.google.com.ai/search*", + "https://www.google.al/search*", + "https://www.google.am/search*", + "https://www.google.co.ao/search*", + "https://www.google.com.ar/search*", + "https://www.google.as/search*", + "https://www.google.at/search*", + "https://www.google.com.au/search*", + "https://www.google.az/search*", + "https://www.google.ba/search*", + "https://www.google.com.bd/search*", + "https://www.google.be/search*", + "https://www.google.bf/search*", + "https://www.google.bg/search*", + "https://www.google.com.bh/search*", + "https://www.google.bi/search*", + "https://www.google.bj/search*", + "https://www.google.com.bn/search*", + "https://www.google.com.bo/search*", + "https://www.google.com.br/search*", + "https://www.google.bs/search*", + "https://www.google.bt/search*", + "https://www.google.co.bw/search*", + "https://www.google.by/search*", + "https://www.google.com.bz/search*", + "https://www.google.ca/search*", + "https://www.google.cd/search*", + "https://www.google.cf/search*", + "https://www.google.cg/search*", + "https://www.google.ch/search*", + "https://www.google.ci/search*", + "https://www.google.co.ck/search*", + "https://www.google.cl/search*", + "https://www.google.cm/search*", + "https://www.google.cn/search*", + "https://www.google.com.co/search*", + "https://www.google.co.cr/search*", + "https://www.google.com.cu/search*", + "https://www.google.cv/search*", + "https://www.google.com.cy/search*", + "https://www.google.cz/search*", + "https://www.google.de/search*", + "https://www.google.dj/search*", + "https://www.google.dk/search*", + "https://www.google.dm/search*", + "https://www.google.com.do/search*", + "https://www.google.dz/search*", + "https://www.google.com.ec/search*", + "https://www.google.ee/search*", + "https://www.google.com.eg/search*", + "https://www.google.es/search*", + "https://www.google.com.et/search*", + "https://www.google.fi/search*", + "https://www.google.com.fj/search*", + "https://www.google.fm/search*", + "https://www.google.fr/search*", + "https://www.google.ga/search*", + "https://www.google.ge/search*", + "https://www.google.gg/search*", + "https://www.google.com.gh/search*", + "https://www.google.com.gi/search*", + "https://www.google.gl/search*", + "https://www.google.gm/search*", + "https://www.google.gr/search*", + "https://www.google.com.gt/search*", + "https://www.google.gy/search*", + "https://www.google.com.hk/search*", + "https://www.google.hn/search*", + "https://www.google.hr/search*", + "https://www.google.ht/search*", + "https://www.google.hu/search*", + "https://www.google.co.id/search*", + "https://www.google.ie/search*", + "https://www.google.co.il/search*", + "https://www.google.im/search*", + "https://www.google.co.in/search*", + "https://www.google.iq/search*", + "https://www.google.is/search*", + "https://www.google.it/search*", + "https://www.google.je/search*", + "https://www.google.com.jm/search*", + "https://www.google.jo/search*", + "https://www.google.co.jp/search*", + "https://www.google.co.ke/search*", + "https://www.google.com.kh/search*", + "https://www.google.ki/search*", + "https://www.google.kg/search*", + "https://www.google.co.kr/search*", + "https://www.google.com.kw/search*", + "https://www.google.kz/search*", + "https://www.google.la/search*", + "https://www.google.com.lb/search*", + "https://www.google.li/search*", + "https://www.google.lk/search*", + "https://www.google.co.ls/search*", + "https://www.google.lt/search*", + "https://www.google.lu/search*", + "https://www.google.lv/search*", + "https://www.google.com.ly/search*", + "https://www.google.co.ma/search*", + "https://www.google.md/search*", + "https://www.google.me/search*", + "https://www.google.mg/search*", + "https://www.google.mk/search*", + "https://www.google.ml/search*", + "https://www.google.com.mm/search*", + "https://www.google.mn/search*", + "https://www.google.ms/search*", + "https://www.google.com.mt/search*", + "https://www.google.mu/search*", + "https://www.google.mv/search*", + "https://www.google.mw/search*", + "https://www.google.com.mx/search*", + "https://www.google.com.my/search*", + "https://www.google.co.mz/search*", + "https://www.google.com.na/search*", + "https://www.google.com.ng/search*", + "https://www.google.com.ni/search*", + "https://www.google.ne/search*", + "https://www.google.nl/search*", + "https://www.google.no/search*", + "https://www.google.com.np/search*", + "https://www.google.nr/search*", + "https://www.google.nu/search*", + "https://www.google.co.nz/search*", + "https://www.google.com.om/search*", + "https://www.google.com.pa/search*", + "https://www.google.com.pe/search*", + "https://www.google.com.pg/search*", + "https://www.google.com.ph/search*", + "https://www.google.com.pk/search*", + "https://www.google.pl/search*", + "https://www.google.pn/search*", + "https://www.google.com.pr/search*", + "https://www.google.ps/search*", + "https://www.google.pt/search*", + "https://www.google.com.py/search*", + "https://www.google.com.qa/search*", + "https://www.google.ro/search*", + "https://www.google.ru/search*", + "https://www.google.rw/search*", + "https://www.google.com.sa/search*", + "https://www.google.com.sb/search*", + "https://www.google.sc/search*", + "https://www.google.se/search*", + "https://www.google.com.sg/search*", + "https://www.google.sh/search*", + "https://www.google.si/search*", + "https://www.google.sk/search*", + "https://www.google.com.sl/search*", + "https://www.google.sn/search*", + "https://www.google.so/search*", + "https://www.google.sm/search*", + "https://www.google.sr/search*", + "https://www.google.st/search*", + "https://www.google.com.sv/search*", + "https://www.google.td/search*", + "https://www.google.tg/search*", + "https://www.google.co.th/search*", + "https://www.google.com.tj/search*", + "https://www.google.tl/search*", + "https://www.google.tm/search*", + "https://www.google.tn/search*", + "https://www.google.to/search*", + "https://www.google.com.tr/search*", + "https://www.google.tt/search*", + "https://www.google.com.tw/search*", + "https://www.google.co.tz/search*", + "https://www.google.com.ua/search*", + "https://www.google.co.ug/search*", + "https://www.google.co.uk/search*", + "https://www.google.com.uy/search*", + "https://www.google.co.uz/search*", + "https://www.google.com.vc/search*", + "https://www.google.co.ve/search*", + "https://www.google.vg/search*", + "https://www.google.co.vi/search*", + "https://www.google.com.vn/search*", + "https://www.google.vu/search*", + "https://www.google.ws/search*", + "https://www.google.rs/search*", + "https://www.google.co.za/search*", + "https://www.google.co.zm/search*", + "https://www.google.co.zw/search*", + "https://www.google.cat/search*", + "https://www.baidu.com/*", + "https://m.baidu.com/*", + "https://*search.yahoo.com/search*", + "https://www.bing.com/search*", + "https://duckduckgo.com/*" + ] +} diff --git a/mobile/android/android-components/components/feature/search/src/main/assets/extensions/search/searchTelemetry.js b/mobile/android/android-components/components/feature/search/src/main/assets/extensions/search/searchTelemetry.js new file mode 100644 index 000000000000..3199335fdfa5 --- /dev/null +++ b/mobile/android/android-components/components/feature/search/src/main/assets/extensions/search/searchTelemetry.js @@ -0,0 +1,61 @@ +/* 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/. */ + +/** + * Send + * - current URL + * - cookies of this page + * to the native application. + */ +function sendCurrentState() { + let message = { + 'url': document.location.href, + 'cookies': getCookies() + }; + browser.runtime.sendNativeMessage("MozacBrowserSearchMessage", message); +} + +/** + * Get all cookies for the current document. + * + * @return {Array<{name: string, value: string}>} containing all cookies. + */ +function getCookies() { + let cookiesList = document.cookie.split("; "); + let result = []; + + cookiesList.forEach(cookie => { + var [name, ...value] = cookie.split('='); + // For that special cases where the cookie value contains '='. + value = value.join("="); + + result.push({ + "name" : name, + "value" : value + }); + }); + + return result; +} + +// Whenever a page is first accessed or when loaded from cache +// send all needed data about the search provider to the app. +const events = ["pageshow", "load"]; +const eventLogger = event => { + switch (event.type) { + case "load": + sendCurrentState(); + break; + case "pageshow": + if (event.persisted) { + sendCurrentState(); + } + break; + default: + console.log('Event:', event.type); + } +}; +events.forEach(eventName => + window.addEventListener(eventName, eventLogger) +); diff --git a/mobile/android/android-components/components/feature/search/src/main/java/mozilla/components/feature/search/middleware/AdsTelemetryMiddleware.kt b/mobile/android/android-components/components/feature/search/src/main/java/mozilla/components/feature/search/middleware/AdsTelemetryMiddleware.kt new file mode 100644 index 000000000000..0625dedc45ba --- /dev/null +++ b/mobile/android/android-components/components/feature/search/src/main/java/mozilla/components/feature/search/middleware/AdsTelemetryMiddleware.kt @@ -0,0 +1,77 @@ +/* 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 mozilla.components.feature.search.middleware + +import androidx.annotation.VisibleForTesting +import mozilla.components.browser.state.action.BrowserAction +import mozilla.components.browser.state.action.ContentAction +import mozilla.components.browser.state.selector.findTab +import mozilla.components.browser.state.state.BrowserState +import mozilla.components.browser.state.store.BrowserStore +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.log.logger.Logger + +/** + * [BrowserStore] middleware to be used alongside with [AdsTelemetry] to check when an ad shown + * in search results is clicked. + */ +class AdsTelemetryMiddleware( + private val adsTelemetry: AdsTelemetry +) : Middleware { + @VisibleForTesting + internal val redirectChain = mutableMapOf() + private val logger = Logger("AdsTelemetryMiddleware") + + @Suppress("TooGenericExceptionCaught") + override fun invoke( + context: MiddlewareContext, + next: (BrowserAction) -> Unit, + action: BrowserAction + ) { + when (action) { + is ContentAction.UpdateLoadRequestAction -> { + context.state.findTab(action.sessionId)?.let { tab -> + // Collect all load requests in between location changes + if (!redirectChain.containsKey(action.sessionId) && action.loadRequest.url != tab.content.url) { + redirectChain[action.sessionId] = RedirectChain(tab.content.url) + } + + redirectChain[action.sessionId]?.add(action.loadRequest.url) + } + } + is ContentAction.UpdateUrlAction -> { + redirectChain[action.sessionId]?.let { + // Record ads telemetry providing all redirects + try { + adsTelemetry.checkIfAddWasClicked(it.root, it.chain) + } catch (t: Throwable) { + logger.info("Failed to record search telemetry", t) + } finally { + redirectChain.remove(action.sessionId) + } + } + } + else -> { + // no-op + } + } + + next(action) + } +} + +/** + * Utility to collect URLs / load requests in between location changes. + */ +@VisibleForTesting +internal class RedirectChain(val root: String) { + val chain = mutableListOf() + + fun add(url: String) { + chain.add(url) + } +} diff --git a/mobile/android/android-components/components/feature/search/src/main/java/mozilla/components/feature/search/telemetry/BaseSearchTelemetry.kt b/mobile/android/android-components/components/feature/search/src/main/java/mozilla/components/feature/search/telemetry/BaseSearchTelemetry.kt new file mode 100644 index 000000000000..0b1fa62675b0 --- /dev/null +++ b/mobile/android/android-components/components/feature/search/src/main/java/mozilla/components/feature/search/telemetry/BaseSearchTelemetry.kt @@ -0,0 +1,174 @@ +/* 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 mozilla.components.feature.search.telemetry + +import androidx.annotation.VisibleForTesting +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.Component +import mozilla.components.support.base.facts.Action.INTERACTION +import mozilla.components.support.base.facts.Fact +import mozilla.components.support.base.facts.collect +import mozilla.components.support.base.log.logger.Logger +import mozilla.components.support.ktx.kotlinx.coroutines.flow.filterChanged +import org.json.JSONObject + +/** + * Main configuration and functionality for tracking ads / web searches with specific providers. + */ +abstract class BaseSearchTelemetry { + + /** + * Install the web extensions that this functionality is based on and start listening for updates. + */ + abstract fun install(engine: Engine, store: BrowserStore) + + @VisibleForTesting(otherwise = VisibleForTesting.PROTECTED) + internal fun getProviderForUrl(url: String): SearchProviderModel? = + providerList.find { provider -> provider.regexp.containsMatchIn(url) } + + @VisibleForTesting(otherwise = VisibleForTesting.PROTECTED) + 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) + }) + } + + protected fun emitFact( + event: String, + value: String, + metadata: Map? = null + ) { + Fact( + Component.FEATURE_SEARCH, + INTERACTION, + event, + value, + metadata + ).collect() + } + + protected sealed class Action + + @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" + ) + ) + ) + + private suspend fun subscribeToUpdates( + flow: Flow, + 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() + ) + } + } + + /** + * 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 { + + @Throws(IllegalStateException::class) + override fun onMessage(message: Any, source: EngineSession?): Any { + if (message is JSONObject) { + processMessage(message) + } else { + throw IllegalStateException("Received unexpected message: $message") + } + + return Unit + } + } +} diff --git a/mobile/android/android-components/components/feature/search/src/main/java/mozilla/components/feature/search/telemetry/ExtensionInfo.kt b/mobile/android/android-components/components/feature/search/src/main/java/mozilla/components/feature/search/telemetry/ExtensionInfo.kt new file mode 100644 index 000000000000..6bac3b15058e --- /dev/null +++ b/mobile/android/android-components/components/feature/search/src/main/java/mozilla/components/feature/search/telemetry/ExtensionInfo.kt @@ -0,0 +1,18 @@ +/* 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 mozilla.components.feature.search.telemetry + +/** + * Configuration data of web extensions used for the search / ads telemetry. + * + * @property id webExtension unique id. + * @property resourceUrl location of the webextension (may be local or web hosted). + * @property messageId message key used for communicating from the extension to the native app. + */ +internal data class ExtensionInfo( + val id: String, + val resourceUrl: String, + val messageId: String +) diff --git a/mobile/android/android-components/components/feature/search/src/main/java/mozilla/components/feature/search/telemetry/SearchProviderCookie.kt b/mobile/android/android-components/components/feature/search/src/main/java/mozilla/components/feature/search/telemetry/SearchProviderCookie.kt new file mode 100644 index 000000000000..20bca4e9d97b --- /dev/null +++ b/mobile/android/android-components/components/feature/search/src/main/java/mozilla/components/feature/search/telemetry/SearchProviderCookie.kt @@ -0,0 +1,26 @@ +/* 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 mozilla.components.feature.search.telemetry + +/** + * Cookie details used to identify follow-on searches. + * + * @property extraCodeParam the query parameter name in the URL that indicates + * this might be a follow-on search. + * @property extraCodePrefixes possible values for the query parameter in the URL that indicates + * this might be a follow-on search. + * @property host the hostname on which the cookie is stored. + * @property name the name of the cookie to check. + * @property codeParam the name of parameter within the cookie. + * @property codePrefixes possible values for the parameter within the cookie. + */ +internal data class SearchProviderCookie( + val extraCodeParam: String, + val extraCodePrefixes: List, + val host: String, + val name: String, + val codeParam: String, + val codePrefixes: List +) diff --git a/mobile/android/android-components/components/feature/search/src/main/java/mozilla/components/feature/search/telemetry/SearchProviderModel.kt b/mobile/android/android-components/components/feature/search/src/main/java/mozilla/components/feature/search/telemetry/SearchProviderModel.kt new file mode 100644 index 000000000000..9242be9a889b --- /dev/null +++ b/mobile/android/android-components/components/feature/search/src/main/java/mozilla/components/feature/search/telemetry/SearchProviderModel.kt @@ -0,0 +1,59 @@ +/* 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 mozilla.components.feature.search.telemetry + +/** + * All data needed to identify ads of a particular provider. + * + * @property name provider name. + * @property regexp regular expression used to match the provider. + * @property queryParam name of the query parameter for the user's search string. + * @property codeParam name of the query parameter for the partner code. + * @property codePrefixes array of prefixes (or complete values) to match against + * the partner code parameters in the url. + * @property followOnParams array of query parameter names that are used when a follow-on search occurs. + * @property extraAdServersRegexps array of regular expressions that match URLs of potential ad servers. + * @property followOnCookies array of cookie details that are used to identify follow-on searches. + */ +internal data class SearchProviderModel( + val name: String, + val regexp: Regex, + val queryParam: String, + val codeParam: String, + val codePrefixes: List, + val followOnParams: List, + val extraAdServersRegexps: List, + val followOnCookies: List +) { + + constructor( + name: String, + regexp: String, + queryParam: String, + codeParam: String = "", + codePrefixes: List = emptyList(), + followOnParams: List = emptyList(), + extraAdServersRegexps: List = emptyList(), + followOnCookies: List = 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) = urlList.any { url -> isAd(url) } + + private fun isAd(url: String) = + extraAdServersRegexps.any { adsRegex -> adsRegex.containsMatchIn(url) } +} diff --git a/mobile/android/android-components/components/feature/search/src/main/java/mozilla/components/feature/search/telemetry/TrackKeyInfo.kt b/mobile/android/android-components/components/feature/search/src/main/java/mozilla/components/feature/search/telemetry/TrackKeyInfo.kt new file mode 100644 index 000000000000..1e57d0536362 --- /dev/null +++ b/mobile/android/android-components/components/feature/search/src/main/java/mozilla/components/feature/search/telemetry/TrackKeyInfo.kt @@ -0,0 +1,36 @@ +/* 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 mozilla.components.feature.search.telemetry + +import java.util.Locale + +/** + * 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: + * `.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 "" + } +} diff --git a/mobile/android/android-components/components/feature/search/src/main/java/mozilla/components/feature/search/telemetry/Utils.kt b/mobile/android/android-components/components/feature/search/src/main/java/mozilla/components/feature/search/telemetry/Utils.kt new file mode 100644 index 000000000000..fc66f7f4859c --- /dev/null +++ b/mobile/android/android-components/components/feature/search/src/main/java/mozilla/components/feature/search/telemetry/Utils.kt @@ -0,0 +1,115 @@ +/* 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 mozilla.components.feature.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" + +/** + * Get a String in a specific format allowing to identify how an ads/search provider was used. + * + * @see [TrackKeyInfo.createTrackKey] + */ +internal fun getTrackKey( + provider: SearchProviderModel, + uri: Uri, + cookies: List +): 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 +): 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, paramSet: Set): 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) } diff --git a/mobile/android/android-components/components/feature/search/src/main/java/mozilla/components/feature/search/telemetry/ads/AdsTelemetry.kt b/mobile/android/android-components/components/feature/search/src/main/java/mozilla/components/feature/search/telemetry/ads/AdsTelemetry.kt new file mode 100644 index 000000000000..9e3be45da2c6 --- /dev/null +++ b/mobile/android/android-components/components/feature/search/src/main/java/mozilla/components/feature/search/telemetry/ads/AdsTelemetry.kt @@ -0,0 +1,119 @@ +/* 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 mozilla.components.feature.search.telemetry.ads + +import android.net.Uri +import androidx.annotation.VisibleForTesting +import mozilla.components.browser.state.store.BrowserStore +import mozilla.components.concept.engine.Engine +import mozilla.components.feature.search.telemetry.BaseSearchTelemetry +import mozilla.components.feature.search.telemetry.ExtensionInfo +import mozilla.components.feature.search.telemetry.getTrackKey +import mozilla.components.support.base.facts.Fact +import mozilla.components.support.ktx.android.org.json.toList +import org.json.JSONObject + +/** + * Telemetry for knowing how often users see/click ads in search and from which provider. + * + * Implemented as a browser extension based on the WebExtension API: + * https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions + */ +class AdsTelemetry : BaseSearchTelemetry() { + + // SERP cached cookies used to check whether an ad was clicked. + @VisibleForTesting + internal var cachedCookies = listOf() + + 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 = message.getJSONArray(ADS_MESSAGE_COOKIES_KEY).toList() + + val urls = message.getJSONArray(ADS_MESSAGE_DOCUMENT_URLS_KEY).toList() + val provider = getProviderForUrl(message.getString(ADS_MESSAGE_SESSION_URL_KEY)) + + provider?.let { + if (it.containsAdLinks(urls)) { + emitFact( + SERP_SHOWN_WITH_ADDS, + it.name + ) + } + } + } + + /** + * To be 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 will be 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. + */ + @Suppress("ReturnCount") + fun checkIfAddWasClicked(url: String?, urlPath: List) { + if (url == null) { + return + } + val uri = Uri.parse(url) ?: 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 + } + + emitFact( + SERP_ADD_CLICKED, + getTrackKey(provider, uri, cachedCookies) + ) + } + + companion object { + /** + * [Fact] property indicating the user open a Search Engine Result Page + * of one of our search providers which contains ads. + */ + const val SERP_SHOWN_WITH_ADDS = "SERP shown with adds" + + /** + * [Fact] property indicating that an ad was clicked in a Search Engine Result Page. + */ + const val SERP_ADD_CLICKED = "SERP add clicked" + + @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_COOKIES_KEY = "cookies" + + @VisibleForTesting + internal const val ADS_MESSAGE_ID = "MozacBrowserAdsMessage" + } +} diff --git a/mobile/android/android-components/components/feature/search/src/main/java/mozilla/components/feature/search/telemetry/incontent/InContentTelemetry.kt b/mobile/android/android-components/components/feature/search/src/main/java/mozilla/components/feature/search/telemetry/incontent/InContentTelemetry.kt new file mode 100644 index 000000000000..68c7cb6caa14 --- /dev/null +++ b/mobile/android/android-components/components/feature/search/src/main/java/mozilla/components/feature/search/telemetry/incontent/InContentTelemetry.kt @@ -0,0 +1,78 @@ +/* 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 mozilla.components.feature.search.telemetry.incontent + +import android.net.Uri +import androidx.annotation.VisibleForTesting +import mozilla.components.browser.state.store.BrowserStore +import mozilla.components.concept.engine.Engine +import mozilla.components.feature.search.telemetry.BaseSearchTelemetry +import mozilla.components.feature.search.telemetry.ExtensionInfo +import mozilla.components.feature.search.telemetry.getTrackKey +import mozilla.components.support.base.facts.Fact +import mozilla.components.support.ktx.android.org.json.toList +import org.json.JSONObject + +/** + * Telemetry for knowing of in-web-content searches (including follow-on searches) and the provider used. + * + * Implemented as a browser extension based on the WebExtension API: + * https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions + */ +class InContentTelemetry : BaseSearchTelemetry() { + + override fun install(engine: Engine, store: BrowserStore) { + val info = ExtensionInfo( + id = SEARCH_EXTENSION_ID, + resourceUrl = SEARCH_EXTENSION_RESOURCE_URL, + messageId = SEARCH_MESSAGE_ID + ) + installWebExtension(engine, store, info) + } + + override fun processMessage(message: JSONObject) { + val cookies = message.getJSONArray(SEARCH_MESSAGE_LIST_KEY).toList() + trackPartnerUrlTypeMetric(message.getString(SEARCH_MESSAGE_SESSION_URL_KEY), cookies) + } + + @VisibleForTesting + internal fun trackPartnerUrlTypeMetric(url: String, cookies: List) { + val provider = getProviderForUrl(url) ?: return + val uri = Uri.parse(url) + val paramSet = uri.queryParameterNames + + if (!paramSet.contains(provider.queryParam)) { + return + } + + emitFact( + IN_CONTENT_SEARCH, + getTrackKey(provider, uri, cookies) + ) + } + + companion object { + /** + * [Fact] property indicating that the user did a search, be it a new one + * or continuing from an existing search. + */ + const val IN_CONTENT_SEARCH = "in content search" + + @VisibleForTesting + internal const val SEARCH_EXTENSION_ID = "cookies@mozac.org" + + @VisibleForTesting + internal const val SEARCH_EXTENSION_RESOURCE_URL = "resource://android/assets/extensions/search/" + + @VisibleForTesting + internal const val SEARCH_MESSAGE_SESSION_URL_KEY = "url" + + @VisibleForTesting + internal const val SEARCH_MESSAGE_LIST_KEY = "cookies" + + @VisibleForTesting + internal const val SEARCH_MESSAGE_ID = "MozacBrowserSearchMessage" + } +} diff --git a/mobile/android/android-components/components/feature/search/src/test/java/mozilla/components/feature/search/middleware/AdsTelemetryMiddlewareTest.kt b/mobile/android/android-components/components/feature/search/src/test/java/mozilla/components/feature/search/middleware/AdsTelemetryMiddlewareTest.kt new file mode 100644 index 000000000000..84e25382d1d6 --- /dev/null +++ b/mobile/android/android-components/components/feature/search/src/test/java/mozilla/components/feature/search/middleware/AdsTelemetryMiddlewareTest.kt @@ -0,0 +1,111 @@ +/* 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 mozilla.components.feature.search.middleware + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import mozilla.components.browser.state.action.ContentAction +import mozilla.components.browser.state.action.TabListAction +import mozilla.components.browser.state.state.BrowserState +import mozilla.components.browser.state.state.ContentState +import mozilla.components.browser.state.state.LoadRequestState +import mozilla.components.browser.state.state.TabSessionState +import mozilla.components.browser.state.state.createTab +import mozilla.components.browser.state.store.BrowserStore +import mozilla.components.feature.search.telemetry.ads.AdsTelemetry +import mozilla.components.support.test.ext.joinBlocking +import mozilla.components.support.test.mock +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNotNull +import org.junit.Assert.assertNull +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Mockito.verify + +@RunWith(AndroidJUnit4::class) +class AdsTelemetryMiddlewareTest { + val sessionId = "session" + lateinit var adsMiddleware: AdsTelemetryMiddleware + lateinit var browserState: BrowserState + + @Before + fun setup() { + adsMiddleware = AdsTelemetryMiddleware(mock()) + browserState = BrowserState( + tabs = listOf(TabSessionState(content = ContentState("https://mozilla.org"), id = sessionId)) + ) + } + + @Test + fun `GIVEN redirectChain empty WHEN a new URL loads THEN the redirectChain starts from the current tab url`() { + val store = BrowserStore( + initialState = browserState, + middleware = listOf(adsMiddleware) + ) + + store.dispatch(ContentAction.UpdateLoadRequestAction(sessionId, LoadRequestState( + url = "https://mozilla.org/firefox", triggeredByRedirect = false, triggeredByUser = false + ))).joinBlocking() + + assertEquals(1, adsMiddleware.redirectChain.size) + assertEquals("https://mozilla.org", adsMiddleware.redirectChain[sessionId]!!.root) + } + + @Test + fun `GIVEN redirectChain is not empty WHEN a new URL loads THEN that URL is added to the chain`() { + adsMiddleware.redirectChain[sessionId] = RedirectChain("https://mozilla.org") + val store = BrowserStore( + initialState = browserState, + middleware = listOf(adsMiddleware) + ) + + store.dispatch(ContentAction.UpdateLoadRequestAction(sessionId, LoadRequestState( + url = "https://mozilla.org/firefox", triggeredByRedirect = false, triggeredByUser = false + ))).joinBlocking() + + assertEquals(1, adsMiddleware.redirectChain.size) + assertEquals("https://mozilla.org", adsMiddleware.redirectChain[sessionId]!!.root) + assertEquals(1, adsMiddleware.redirectChain.size) + assertEquals("https://mozilla.org/firefox", adsMiddleware.redirectChain[sessionId]!!.chain[0]) + } + + @Test + fun `WHEN the session URL is updated THEN check if an ad was clicked`() { + val adsTelemetry: AdsTelemetry = mock() + val adsMiddleware = AdsTelemetryMiddleware(adsTelemetry) + adsMiddleware.redirectChain[sessionId] = RedirectChain("https://mozilla.org") + adsMiddleware.redirectChain[sessionId]!!.chain.add("https://mozilla.org/firefox") + val store = BrowserStore( + initialState = browserState, + middleware = listOf(adsMiddleware) + ) + + store + .dispatch(ContentAction.UpdateUrlAction(sessionId, "https://mozilla.org/firefox")) + .joinBlocking() + + verify(adsTelemetry).checkIfAddWasClicked( + "https://mozilla.org", listOf("https://mozilla.org/firefox") + ) + } + + @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") + val store = BrowserStore( + initialState = browserState, + middleware = listOf(adsMiddleware) + ) + store.dispatch(TabListAction.AddTabAction(tab)).joinBlocking() + store.dispatch(ContentAction.UpdateLoadRequestAction( + tab.id, LoadRequestState("https://mozilla.org", true, true)) + ).joinBlocking() + + assertNotNull(adsMiddleware.redirectChain[tab.id]) + + store.dispatch(ContentAction.UpdateUrlAction(tab.id, "https://mozilla.org")).joinBlocking() + assertNull(adsMiddleware.redirectChain[tab.id]) + } +} diff --git a/mobile/android/android-components/components/feature/search/src/test/java/mozilla/components/feature/search/telemetry/BaseSearchTelemetryTest.kt b/mobile/android/android-components/components/feature/search/src/test/java/mozilla/components/feature/search/telemetry/BaseSearchTelemetryTest.kt new file mode 100644 index 000000000000..9f9dce2a4242 --- /dev/null +++ b/mobile/android/android-components/components/feature/search/src/test/java/mozilla/components/feature/search/telemetry/BaseSearchTelemetryTest.kt @@ -0,0 +1,87 @@ +/* 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 mozilla.components.feature.search.telemetry + +import mozilla.components.browser.state.store.BrowserStore +import mozilla.components.concept.engine.Engine +import mozilla.components.support.test.any +import mozilla.components.support.test.eq +import mozilla.components.support.test.mock +import org.json.JSONObject +import org.junit.Assert.assertEquals +import org.junit.Before +import org.junit.Test +import org.mockito.Mockito.spy +import org.mockito.Mockito.verify + +class BaseSearchTelemetryTest { + + private lateinit var baseTelemetry: BaseSearchTelemetry + private lateinit var handler: BaseSearchTelemetry.SearchTelemetryMessageHandler + + @Before + fun setup() { + baseTelemetry = spy(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 `GIVEN an engine WHEN installWebExtension is called THEN the provided extension is installed in engine`() { + val engine: Engine = mock() + val store: BrowserStore = mock() + val id = "id" + val resourceUrl = "resourceUrl" + val messageId = "messageId" + val extensionInfo = ExtensionInfo(id, resourceUrl, messageId) + + baseTelemetry.installWebExtension(engine, store, extensionInfo) + + verify(engine).installWebExtension( + id = eq(id), + url = eq(resourceUrl), + onSuccess = any(), + onError = any() + ) + } + + @Test + fun `GIVEN a search provider does not exist for the url WHEN getProviderForUrl is called THEN return null`() { + val url = "https://www.mozilla.com/search?q=firefox" + + assertEquals(null, baseTelemetry.getProviderForUrl(url)) + } + + @Test + fun `GIVEN a search provider exists for the url WHEN getProviderForUrl is called THEN return that provider`() { + val url = "https://www.google.com/search?q=computers" + + assertEquals("google", baseTelemetry.getProviderForUrl(url)?.name) + } + + @Test(expected = IllegalStateException::class) + fun `GIVEN an extension message WHEN that cannot be processed THEN throw IllegalStateException`() { + val message = "message" + + handler.onMessage(message, mock()) + } + + @Test + fun `GIVEN an extension message WHEN received THEN pass it to processMessage`() { + val message = JSONObject() + + handler.onMessage(message, mock()) + + verify(baseTelemetry).processMessage(message) + } +} diff --git a/mobile/android/android-components/components/feature/search/src/test/java/mozilla/components/feature/search/telemetry/SearchProviderModelTest.kt b/mobile/android/android-components/components/feature/search/src/test/java/mozilla/components/feature/search/telemetry/SearchProviderModelTest.kt new file mode 100644 index 000000000000..1fcda782b3c0 --- /dev/null +++ b/mobile/android/android-components/components/feature/search/src/test/java/mozilla/components/feature/search/telemetry/SearchProviderModelTest.kt @@ -0,0 +1,38 @@ +/* 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 mozilla.components.feature.search.telemetry + +import org.junit.Assert +import org.junit.Test + +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" + Assert.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/" + Assert.assertFalse(testSearchProvider.containsAdLinks(listOf(nonAd1, nonAd2))) + } +} diff --git a/mobile/android/android-components/components/feature/search/src/test/java/mozilla/components/feature/search/telemetry/ads/AdsTelemetryTest.kt b/mobile/android/android-components/components/feature/search/src/test/java/mozilla/components/feature/search/telemetry/ads/AdsTelemetryTest.kt new file mode 100644 index 000000000000..4f8c4793f653 --- /dev/null +++ b/mobile/android/android-components/components/feature/search/src/test/java/mozilla/components/feature/search/telemetry/ads/AdsTelemetryTest.kt @@ -0,0 +1,190 @@ +/* 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 mozilla.components.feature.search.telemetry.ads + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import mozilla.components.browser.state.store.BrowserStore +import mozilla.components.concept.engine.Engine +import mozilla.components.feature.search.telemetry.ExtensionInfo +import mozilla.components.feature.search.telemetry.ads.AdsTelemetry.Companion.ADS_EXTENSION_ID +import mozilla.components.feature.search.telemetry.ads.AdsTelemetry.Companion.ADS_EXTENSION_RESOURCE_URL +import mozilla.components.feature.search.telemetry.ads.AdsTelemetry.Companion.ADS_MESSAGE_COOKIES_KEY +import mozilla.components.feature.search.telemetry.ads.AdsTelemetry.Companion.ADS_MESSAGE_DOCUMENT_URLS_KEY +import mozilla.components.feature.search.telemetry.ads.AdsTelemetry.Companion.ADS_MESSAGE_ID +import mozilla.components.feature.search.telemetry.ads.AdsTelemetry.Companion.ADS_MESSAGE_SESSION_URL_KEY +import mozilla.components.support.base.Component +import mozilla.components.support.base.facts.Action +import mozilla.components.support.base.facts.Fact +import mozilla.components.support.base.facts.FactProcessor +import mozilla.components.support.base.facts.Facts +import mozilla.components.support.test.argumentCaptor +import mozilla.components.support.test.eq +import mozilla.components.support.test.mock +import org.json.JSONArray +import org.json.JSONObject +import org.junit.Assert.assertEquals +import org.junit.Assert.assertTrue +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Mockito.spy +import org.mockito.Mockito.verify + +@RunWith(AndroidJUnit4::class) +class AdsTelemetryTest { + private lateinit var telemetry: AdsTelemetry + + @Before + fun setUp() { + telemetry = spy(AdsTelemetry()) + } + + @Test + fun `WHEN installWebExtension is called THEN install a properly configured extension`() { + val engine: Engine = mock() + val store: BrowserStore = mock() + val extensionCaptor = argumentCaptor() + + telemetry.install(engine, store) + + verify(telemetry).installWebExtension(eq(engine), eq(store), extensionCaptor.capture()) + assertEquals(ADS_EXTENSION_ID, extensionCaptor.value.id) + assertEquals(ADS_EXTENSION_RESOURCE_URL, extensionCaptor.value.resourceUrl) + assertEquals(ADS_MESSAGE_ID, extensionCaptor.value.messageId) + } + + @Test + fun `WHEN checkIfAddWasClicked is called with a null session URL THEN don't emit a Fact`() { + val facts = mutableListOf() + Facts.registerProcessor(object : FactProcessor { + override fun process(fact: Fact) { + facts.add(fact) + } + }) + + telemetry.checkIfAddWasClicked(null, listOf()) + + assertTrue(facts.isEmpty()) + } + + @Test + fun `GIVEN no ads in the redirect path WHEN checkIfAddWasClicked is called THEN don't emit a Fact`() { + val sessionUrl = "https://www.google.com/search?q=aaa" + val facts = mutableListOf() + Facts.registerProcessor(object : FactProcessor { + override fun process(fact: Fact) { + facts.add(fact) + } + }) + + telemetry.checkIfAddWasClicked(sessionUrl, listOf("https://www.aaa.com")) + + assertTrue(facts.isEmpty()) + } + + @Test + fun `GIVEN ads are in the redirect path WHEN checkIfAddWasClicked is called THEN emit an appropriate SERP_ADD_CLICKED Fact`() { + val sessionUrl = "https://www.google.com/search?q=aaa" + val facts = mutableListOf() + Facts.registerProcessor(object : FactProcessor { + override fun process(fact: Fact) { + facts.add(fact) + } + }) + + telemetry.checkIfAddWasClicked( + sessionUrl, + listOf("https://www.google.com/aclk", "https://www.aaa.com") + ) + + assertEquals(1, facts.size) + assertEquals(Component.FEATURE_SEARCH, facts[0].component) + assertEquals(Action.INTERACTION, facts[0].action) + assertEquals(AdsTelemetry.SERP_ADD_CLICKED, facts[0].item) + assertEquals("google.in-content.organic.none", facts[0].value) + } + + @Test + fun `GIVEN a message containing ad links from the extension WHEN processMessage is called THEN track a SERP_SHOWN_WITH_ADDS Fact`() { + 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) + val facts = mutableListOf() + Facts.registerProcessor(object : FactProcessor { + override fun process(fact: Fact) { + facts.add(fact) + } + }) + + telemetry.processMessage(message) + + assertEquals(1, facts.size) + assertEquals(Component.FEATURE_SEARCH, facts[0].component) + assertEquals(Action.INTERACTION, facts[0].action) + assertEquals(AdsTelemetry.SERP_SHOWN_WITH_ADDS, facts[0].item) + assertEquals(telemetry.providerList[0].name, facts[0].value) + } + + @Test + fun `GIVEN a message not containing ad links from the extension WHEN processMessage is called THEN don't emit any Fact`() { + 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) + val facts = mutableListOf() + Facts.registerProcessor(object : FactProcessor { + override fun process(fact: Fact) { + facts.add(fact) + } + }) + + telemetry.processMessage(message) + + assertTrue(facts.isEmpty()) + } + + @Test + fun `GIVEN a Bing sap-follow-on with cookies WHEN checkIfAddWasClicked is called THEN emit an appropriate SERP_ADD_CLICKED Fact`() { + val url = "https://www.bing.com/search?q=aaa&pc=MOZMBA&form=QBRERANDOM" + telemetry.cachedCookies = createCookieList() + val facts = mutableListOf() + Facts.registerProcessor(object : FactProcessor { + override fun process(fact: Fact) { + facts.add(fact) + } + }) + + telemetry.checkIfAddWasClicked(url, listOf("https://www.bing.com/aclik", "https://www.aaa.com")) + + assertEquals(1, facts.size) + assertEquals(Component.FEATURE_SEARCH, facts[0].component) + assertEquals(Action.INTERACTION, facts[0].action) + assertEquals(AdsTelemetry.SERP_ADD_CLICKED, facts[0].item) + assertEquals("bing.in-content.sap-follow-on.mozmba", facts[0].value) + } + + private fun createCookieList(): List { + 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) + } +} diff --git a/mobile/android/android-components/components/feature/search/src/test/java/mozilla/components/feature/search/telemetry/incontent/InContentTelemetryTest.kt b/mobile/android/android-components/components/feature/search/src/test/java/mozilla/components/feature/search/telemetry/incontent/InContentTelemetryTest.kt new file mode 100644 index 000000000000..091d35986cbc --- /dev/null +++ b/mobile/android/android-components/components/feature/search/src/test/java/mozilla/components/feature/search/telemetry/incontent/InContentTelemetryTest.kt @@ -0,0 +1,310 @@ +/* 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 mozilla.components.feature.search.telemetry.incontent + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import mozilla.components.browser.state.store.BrowserStore +import mozilla.components.concept.engine.Engine +import mozilla.components.feature.search.telemetry.ExtensionInfo +import mozilla.components.feature.search.telemetry.incontent.InContentTelemetry.Companion.SEARCH_EXTENSION_ID +import mozilla.components.feature.search.telemetry.incontent.InContentTelemetry.Companion.SEARCH_EXTENSION_RESOURCE_URL +import mozilla.components.feature.search.telemetry.incontent.InContentTelemetry.Companion.SEARCH_MESSAGE_ID +import mozilla.components.feature.search.telemetry.incontent.InContentTelemetry.Companion.SEARCH_MESSAGE_LIST_KEY +import mozilla.components.feature.search.telemetry.incontent.InContentTelemetry.Companion.SEARCH_MESSAGE_SESSION_URL_KEY +import mozilla.components.support.base.Component +import mozilla.components.support.base.facts.Action +import mozilla.components.support.base.facts.Fact +import mozilla.components.support.base.facts.FactProcessor +import mozilla.components.support.base.facts.Facts +import mozilla.components.support.test.argumentCaptor +import mozilla.components.support.test.eq +import mozilla.components.support.test.mock +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.mockito.Mockito.spy +import org.mockito.Mockito.verify + +@RunWith(AndroidJUnit4::class) +class InContentTelemetryTest { + private lateinit var telemetry: InContentTelemetry + + @Before + fun setup() { + telemetry = spy(InContentTelemetry()) + } + + @Test + fun `WHEN installWebExtension is called THEN install a properly configured extension`() { + val engine: Engine = mock() + val store: BrowserStore = mock() + val extensionCaptor = argumentCaptor() + + telemetry.install(engine, store) + + verify(telemetry).installWebExtension(eq(engine), eq(store), extensionCaptor.capture()) + assertEquals(SEARCH_EXTENSION_ID, extensionCaptor.value.id) + assertEquals(SEARCH_EXTENSION_RESOURCE_URL, extensionCaptor.value.resourceUrl) + assertEquals(SEARCH_MESSAGE_ID, extensionCaptor.value.messageId) + } + + @Test + fun `GIVEN a message from the extension WHEN processMessage is called THEN track the search`() { + 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(SEARCH_MESSAGE_LIST_KEY, array) + message.put(SEARCH_MESSAGE_SESSION_URL_KEY, url) + + telemetry.processMessage(message) + + verify(telemetry).trackPartnerUrlTypeMetric(url, listOf(first, second)) + } + + @Test + fun `GIVEN a Google search WHEN trackPartnerUrlTypeMetric is called THEN emit an appropriate IN_CONTENT_SEARCH fact`() { + val url = "https://www.google.com/search?q=aaa&client=firefox-b-m" + val facts = mutableListOf() + Facts.registerProcessor(object : FactProcessor { + override fun process(fact: Fact) { + facts.add(fact) + } + }) + + telemetry.trackPartnerUrlTypeMetric(url, listOf()) + + assertEquals(1, facts.size) + assertEquals(Component.FEATURE_SEARCH, facts[0].component) + assertEquals(Action.INTERACTION, facts[0].action) + assertEquals(InContentTelemetry.IN_CONTENT_SEARCH, facts[0].item) + assertEquals("google.in-content.sap.firefox-b-m", facts[0].value) + } + + @Test + fun `GIVEN a DuckDuckGo search WHEN trackPartnerUrlTypeMetric is called THEN emit an appropriate IN_CONTENT_SEARCH fact`() { + val url = "https://duckduckgo.com/?q=aaa&t=fpas" + val facts = mutableListOf() + Facts.registerProcessor(object : FactProcessor { + override fun process(fact: Fact) { + facts.add(fact) + } + }) + + telemetry.trackPartnerUrlTypeMetric(url, listOf()) + + assertEquals(1, facts.size) + assertEquals(Component.FEATURE_SEARCH, facts[0].component) + assertEquals(Action.INTERACTION, facts[0].action) + assertEquals(InContentTelemetry.IN_CONTENT_SEARCH, facts[0].item) + assertEquals("duckduckgo.in-content.sap.fpas", facts[0].value) + } + + @Test + fun `GIVEN a Baidu search WHEN trackPartnerUrlTypeMetric is called THEN emit an appropriate IN_CONTENT_SEARCH fact`() { + val url = "https://m.baidu.com/s?from=1000969a&word=aaa" + val facts = mutableListOf() + Facts.registerProcessor(object : FactProcessor { + override fun process(fact: Fact) { + facts.add(fact) + } + }) + + telemetry.trackPartnerUrlTypeMetric(url, listOf()) + + assertEquals(1, facts.size) + assertEquals(Component.FEATURE_SEARCH, facts[0].component) + assertEquals(Action.INTERACTION, facts[0].action) + assertEquals(InContentTelemetry.IN_CONTENT_SEARCH, facts[0].item) + assertEquals("baidu.in-content.sap._1000969a", facts[0].value) + } + + @Test + fun `GIVEN a Bing search WHEN trackPartnerUrlTypeMetric is called THEN emit an appropriate IN_CONTENT_SEARCH fact`() { + val url = "https://www.bing.com/search?q=aaa&pc=MOZMBA" + val facts = mutableListOf() + Facts.registerProcessor(object : FactProcessor { + override fun process(fact: Fact) { + facts.add(fact) + } + }) + + telemetry.trackPartnerUrlTypeMetric(url, listOf()) + + assertEquals(1, facts.size) + assertEquals(Component.FEATURE_SEARCH, facts[0].component) + assertEquals(Action.INTERACTION, facts[0].action) + assertEquals(InContentTelemetry.IN_CONTENT_SEARCH, facts[0].item) + assertEquals("bing.in-content.sap.mozmba", facts[0].value) + } + + @Test + fun `GIVEN a Google sap-follow-on WHEN trackPartnerUrlTypeMetric is called THEN emit an appropriate IN_CONTENT_SEARCH fact`() { + val url = "https://www.google.com/search?q=aaa&client=firefox-b-m&oq=random" + val facts = mutableListOf() + Facts.registerProcessor(object : FactProcessor { + override fun process(fact: Fact) { + facts.add(fact) + } + }) + + telemetry.trackPartnerUrlTypeMetric(url, listOf()) + + assertEquals(1, facts.size) + assertEquals(Component.FEATURE_SEARCH, facts[0].component) + assertEquals(Action.INTERACTION, facts[0].action) + assertEquals(InContentTelemetry.IN_CONTENT_SEARCH, facts[0].item) + assertEquals("google.in-content.sap-follow-on.firefox-b-m", facts[0].value) + } + + @Test + fun `GIVEN a Google sap-follow-on from topSite WHEN trackPartnerUrlTypeMetric is called THEN emit an appropriate IN_CONTENT_SEARCH fact`() { + val url = "https://www.google.com/search?q=aaa&client=firefox-b-m&channel=ts&oq=random" + val facts = mutableListOf() + Facts.registerProcessor(object : FactProcessor { + override fun process(fact: Fact) { + facts.add(fact) + } + }) + + telemetry.trackPartnerUrlTypeMetric(url, listOf()) + + assertEquals(1, facts.size) + assertEquals(Component.FEATURE_SEARCH, facts[0].component) + assertEquals(Action.INTERACTION, facts[0].action) + assertEquals(InContentTelemetry.IN_CONTENT_SEARCH, facts[0].item) + assertEquals("google.in-content.sap-follow-on.firefox-b-m.ts", facts[0].value) + } + + @Test + fun `GIVEN a Baidu sap-follow-on WHEN trackPartnerUrlTypeMetric is called THEN emit an appropriate IN_CONTENT_SEARCH fact`() { + val url = "https://m.baidu.com/s?from=1000969a&word=aaa&oq=random" + val facts = mutableListOf() + Facts.registerProcessor(object : FactProcessor { + override fun process(fact: Fact) { + facts.add(fact) + } + }) + + telemetry.trackPartnerUrlTypeMetric(url, listOf()) + + assertEquals(1, facts.size) + assertEquals(Component.FEATURE_SEARCH, facts[0].component) + assertEquals(Action.INTERACTION, facts[0].action) + assertEquals(InContentTelemetry.IN_CONTENT_SEARCH, facts[0].item) + assertEquals("baidu.in-content.sap-follow-on._1000969a", facts[0].value) + } + + @Test + fun `GIVEN a Bing sap-follow-on with cookies WHEN trackPartnerUrlTypeMetric is called THEN emit an appropriate IN_CONTENT_SEARCH fact`() { + val url = "https://www.bing.com/search?q=aaa&pc=MOZMBA&form=QBRERANDOM" + val facts = mutableListOf() + Facts.registerProcessor(object : FactProcessor { + override fun process(fact: Fact) { + facts.add(fact) + } + }) + + telemetry.trackPartnerUrlTypeMetric(url, createCookieList()) + + assertEquals(1, facts.size) + assertEquals(Component.FEATURE_SEARCH, facts[0].component) + assertEquals(Action.INTERACTION, facts[0].action) + assertEquals(InContentTelemetry.IN_CONTENT_SEARCH, facts[0].item) + assertEquals("bing.in-content.sap-follow-on.mozmba", facts[0].value) + } + + @Test + fun `GIVEN a Google organic search WHEN trackPartnerUrlTypeMetric is called THEN emit an appropriate IN_CONTENT_SEARCH fact`() { + val url = "https://www.google.com/search?q=aaa" + val facts = mutableListOf() + Facts.registerProcessor(object : FactProcessor { + override fun process(fact: Fact) { + facts.add(fact) + } + }) + + telemetry.trackPartnerUrlTypeMetric(url, listOf()) + + assertEquals(1, facts.size) + assertEquals(Component.FEATURE_SEARCH, facts[0].component) + assertEquals(Action.INTERACTION, facts[0].action) + assertEquals(InContentTelemetry.IN_CONTENT_SEARCH, facts[0].item) + assertEquals("google.in-content.organic.none", facts[0].value) + } + + @Test + fun `GIVEN a DuckDuckGo organic search WHEN trackPartnerUrlTypeMetric is called THEN emit an appropriate IN_CONTENT_SEARCH fact`() { + val url = "https://duckduckgo.com/?q=aaa" + val facts = mutableListOf() + Facts.registerProcessor(object : FactProcessor { + override fun process(fact: Fact) { + facts.add(fact) + } + }) + + telemetry.trackPartnerUrlTypeMetric(url, listOf()) + + assertEquals(1, facts.size) + assertEquals(Component.FEATURE_SEARCH, facts[0].component) + assertEquals(Action.INTERACTION, facts[0].action) + assertEquals(InContentTelemetry.IN_CONTENT_SEARCH, facts[0].item) + assertEquals("duckduckgo.in-content.organic.none", facts[0].value) + } + + @Test + fun `GIVEN a Bing organic search WHEN trackPartnerUrlTypeMetric is called THEN emit an appropriate IN_CONTENT_SEARCH fact`() { + val url = "https://www.bing.com/search?q=aaa" + val facts = mutableListOf() + Facts.registerProcessor(object : FactProcessor { + override fun process(fact: Fact) { + facts.add(fact) + } + }) + + telemetry.trackPartnerUrlTypeMetric(url, listOf()) + + assertEquals(1, facts.size) + assertEquals(Component.FEATURE_SEARCH, facts[0].component) + assertEquals(Action.INTERACTION, facts[0].action) + assertEquals(InContentTelemetry.IN_CONTENT_SEARCH, facts[0].item) + assertEquals("bing.in-content.organic.none", facts[0].value) + } + + @Test + fun `GIVEN a Baidu organic search WHEN trackPartnerUrlTypeMetric is called THEN emit an appropriate IN_CONTENT_SEARCH fact`() { + val url = "https://m.baidu.com/s?word=aaa" + val facts = mutableListOf() + Facts.registerProcessor(object : FactProcessor { + override fun process(fact: Fact) { + facts.add(fact) + } + }) + + telemetry.trackPartnerUrlTypeMetric(url, listOf()) + + assertEquals(1, facts.size) + assertEquals(Component.FEATURE_SEARCH, facts[0].component) + assertEquals(Action.INTERACTION, facts[0].action) + assertEquals(InContentTelemetry.IN_CONTENT_SEARCH, facts[0].item) + assertEquals("baidu.in-content.organic.none", facts[0].value) + } + + private fun createCookieList(): List { + 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) + } +} diff --git a/mobile/android/android-components/docs/changelog.md b/mobile/android/android-components/docs/changelog.md index 6cd0afb7701a..502585dee34e 100644 --- a/mobile/android/android-components/docs/changelog.md +++ b/mobile/android/android-components/docs/changelog.md @@ -11,6 +11,10 @@ permalink: /changelog/ * [Gecko](https://github.com/mozilla-mobile/android-components/blob/master/buildSrc/src/main/java/Gecko.kt) * [Configuration](https://github.com/mozilla-mobile/android-components/blob/master/.config.yml) +* **feature-search** + * 🌟️ New `AdsTelemetry` based on a web extension that identify whether there are ads in search results of particular providers for which a (Component.FEATURE_SEARCH to SERP_SHOWN_WITH_ADDS) Fact will be emitted and whether an ad was clicked for which a (Component.FEATURE_SEARCH to SERP_ADD_CLICKED) Fact will be emitted if the `AdsTelemetryMiddleware` is set for `BrowserStore`. + * 🌟️ New `InContentTelemetry` based on a web extension that identify follow-on and organic web searches for which a (Component.FEATURE_SEARCH to IN_CONTENT_SEARCH) Fact will be emitted. + # 91.0.0 * [Commits](https://github.com/mozilla-mobile/android-components/compare/v91.0.0...master) * [Milestone](https://github.com/mozilla-mobile/android-components/milestone/138?closed=1) -- 2.11.4.GIT