You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
iceraven-browser/app/src/main/java/org/mozilla/fenix/components/metrics/MetricController.kt

425 lines
19 KiB
Kotlin

/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package org.mozilla.fenix.components.metrics
import androidx.annotation.VisibleForTesting
import mozilla.components.browser.menu.facts.BrowserMenuFacts
import mozilla.components.concept.awesomebar.AwesomeBar
import mozilla.components.feature.autofill.facts.AutofillFacts
import mozilla.components.feature.awesomebar.facts.AwesomeBarFacts
import mozilla.components.feature.awesomebar.provider.BookmarksStorageSuggestionProvider
import mozilla.components.feature.awesomebar.provider.ClipboardSuggestionProvider
import mozilla.components.feature.awesomebar.provider.HistoryStorageSuggestionProvider
import mozilla.components.feature.awesomebar.provider.SearchSuggestionProvider
import mozilla.components.feature.awesomebar.provider.SessionSuggestionProvider
import mozilla.components.feature.contextmenu.facts.ContextMenuFacts
import mozilla.components.feature.media.facts.MediaFacts
import mozilla.components.feature.prompts.dialog.LoginDialogFacts
import mozilla.components.feature.prompts.facts.AddressAutofillDialogFacts
import mozilla.components.feature.prompts.facts.CreditCardAutofillDialogFacts
import mozilla.components.feature.pwa.ProgressiveWebAppFacts
import mozilla.components.feature.search.telemetry.ads.AdsTelemetry
import mozilla.components.feature.search.telemetry.incontent.InContentTelemetry
import mozilla.components.feature.sitepermissions.SitePermissionsFacts
import mozilla.components.feature.syncedtabs.facts.SyncedTabsFacts
import mozilla.components.feature.top.sites.facts.TopSitesFacts
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.base.log.logger.Logger
import mozilla.components.support.webextensions.facts.WebExtensionFacts
import mozilla.telemetry.glean.private.NoExtras
import org.mozilla.fenix.BuildConfig
import org.mozilla.fenix.GleanMetrics.Addons
import org.mozilla.fenix.GleanMetrics.Addresses
import org.mozilla.fenix.GleanMetrics.AndroidAutofill
import org.mozilla.fenix.GleanMetrics.Awesomebar
import org.mozilla.fenix.GleanMetrics.BrowserSearch
import org.mozilla.fenix.GleanMetrics.ContextMenu
import org.mozilla.fenix.GleanMetrics.ContextualMenu
import org.mozilla.fenix.GleanMetrics.CreditCards
import org.mozilla.fenix.GleanMetrics.LoginDialog
import org.mozilla.fenix.GleanMetrics.MediaNotification
import org.mozilla.fenix.GleanMetrics.MediaState
import org.mozilla.fenix.GleanMetrics.PerfAwesomebar
import org.mozilla.fenix.GleanMetrics.ProgressiveWebApp
import org.mozilla.fenix.GleanMetrics.SitePermissions
import org.mozilla.fenix.GleanMetrics.SyncedTabs
import org.mozilla.fenix.search.awesomebar.ShortcutsSuggestionProvider
import org.mozilla.fenix.utils.Settings
import mozilla.components.compose.browser.awesomebar.AwesomeBarFacts as ComposeAwesomeBarFacts
interface MetricController {
fun start(type: MetricServiceType)
fun stop(type: MetricServiceType)
fun track(event: Event)
companion object {
fun create(
services: List<MetricsService>,
isDataTelemetryEnabled: () -> Boolean,
isMarketingDataTelemetryEnabled: () -> Boolean,
settings: Settings,
): MetricController {
return if (BuildConfig.TELEMETRY) {
ReleaseMetricController(
services,
isDataTelemetryEnabled,
isMarketingDataTelemetryEnabled,
settings,
)
} else {
DebugMetricController()
}
}
}
}
@VisibleForTesting
internal class DebugMetricController(
private val logger: Logger = Logger(),
) : MetricController {
override fun start(type: MetricServiceType) {
logger.debug("DebugMetricController: start")
}
override fun stop(type: MetricServiceType) {
logger.debug("DebugMetricController: stop")
}
override fun track(event: Event) {
logger.debug("DebugMetricController: track event: $event")
}
}
@VisibleForTesting
@Suppress("LargeClass")
internal class ReleaseMetricController(
private val services: List<MetricsService>,
private val isDataTelemetryEnabled: () -> Boolean,
private val isMarketingDataTelemetryEnabled: () -> Boolean,
private val settings: Settings,
) : MetricController {
private var initialized = mutableSetOf<MetricServiceType>()
init {
Facts.registerProcessor(
object : FactProcessor {
override fun process(fact: Fact) {
fact.process()
}
},
)
}
@VisibleForTesting
@Suppress("LongMethod")
internal fun Fact.process(): Unit = when (component to item) {
Component.FEATURE_PROMPTS to LoginDialogFacts.Items.DISPLAY -> {
LoginDialog.displayed.record(NoExtras())
}
Component.FEATURE_PROMPTS to LoginDialogFacts.Items.CANCEL -> {
LoginDialog.cancelled.record(NoExtras())
}
Component.FEATURE_PROMPTS to LoginDialogFacts.Items.NEVER_SAVE -> {
LoginDialog.neverSave.record(NoExtras())
}
Component.FEATURE_PROMPTS to LoginDialogFacts.Items.SAVE -> {
LoginDialog.saved.record(NoExtras())
}
Component.FEATURE_MEDIA to MediaFacts.Items.STATE -> {
when (action) {
Action.PLAY -> MediaState.play.record(NoExtras())
Action.PAUSE -> MediaState.pause.record(NoExtras())
Action.STOP -> MediaState.stop.record(NoExtras())
else -> Unit
}
}
Component.FEATURE_MEDIA to MediaFacts.Items.NOTIFICATION -> {
when (action) {
Action.PLAY -> MediaNotification.play.record(NoExtras())
Action.PAUSE -> MediaNotification.pause.record(NoExtras())
else -> Unit
}
}
Component.FEATURE_CONTEXTMENU to ContextMenuFacts.Items.ITEM -> {
metadata?.get("item")?.let { item ->
contextMenuAllowList[item]?.let { extraKey ->
ContextMenu.itemTapped.record(ContextMenu.ItemTappedExtra(extraKey))
}
}
Unit
}
Component.BROWSER_MENU to BrowserMenuFacts.Items.WEB_EXTENSION_MENU_ITEM -> {
metadata?.get("id")?.let {
Addons.openAddonInToolbarMenu.record(Addons.OpenAddonInToolbarMenuExtra(it.toString()))
}
Unit
}
Component.FEATURE_PROMPTS to CreditCardAutofillDialogFacts.Items.AUTOFILL_CREDIT_CARD_FORM_DETECTED ->
CreditCards.formDetected.record(NoExtras())
Component.FEATURE_PROMPTS to CreditCardAutofillDialogFacts.Items.AUTOFILL_CREDIT_CARD_SUCCESS ->
CreditCards.autofilled.record(NoExtras())
Component.FEATURE_PROMPTS to CreditCardAutofillDialogFacts.Items.AUTOFILL_CREDIT_CARD_PROMPT_SHOWN ->
CreditCards.autofillPromptShown.record(NoExtras())
Component.FEATURE_PROMPTS to CreditCardAutofillDialogFacts.Items.AUTOFILL_CREDIT_CARD_PROMPT_EXPANDED ->
CreditCards.autofillPromptExpanded.record(NoExtras())
Component.FEATURE_PROMPTS to CreditCardAutofillDialogFacts.Items.AUTOFILL_CREDIT_CARD_PROMPT_DISMISSED ->
CreditCards.autofillPromptDismissed.record(NoExtras())
Component.FEATURE_PROMPTS to CreditCardAutofillDialogFacts.Items.AUTOFILL_CREDIT_CARD_CREATED ->
CreditCards.savePromptCreate.record(NoExtras())
Component.FEATURE_PROMPTS to CreditCardAutofillDialogFacts.Items.AUTOFILL_CREDIT_CARD_UPDATED ->
CreditCards.savePromptUpdate.record(NoExtras())
Component.FEATURE_PROMPTS to CreditCardAutofillDialogFacts.Items.AUTOFILL_CREDIT_CARD_SAVE_PROMPT_SHOWN ->
CreditCards.savePromptShown.record(NoExtras())
Component.FEATURE_PROMPTS to AddressAutofillDialogFacts.Items.AUTOFILL_ADDRESS_FORM_DETECTED ->
Addresses.formDetected.record(NoExtras())
Component.FEATURE_PROMPTS to AddressAutofillDialogFacts.Items.AUTOFILL_ADDRESS_SUCCESS ->
Addresses.autofilled.record(NoExtras())
Component.FEATURE_PROMPTS to AddressAutofillDialogFacts.Items.AUTOFILL_ADDRESS_PROMPT_SHOWN ->
Addresses.autofillPromptShown.record(NoExtras())
Component.FEATURE_PROMPTS to AddressAutofillDialogFacts.Items.AUTOFILL_ADDRESS_PROMPT_EXPANDED ->
Addresses.autofillPromptExpanded.record(NoExtras())
Component.FEATURE_PROMPTS to AddressAutofillDialogFacts.Items.AUTOFILL_ADDRESS_PROMPT_DISMISSED ->
Addresses.autofillPromptDismissed.record(NoExtras())
Component.FEATURE_AUTOFILL to AutofillFacts.Items.AUTOFILL_REQUEST -> {
val hasMatchingLogins =
metadata?.get(AutofillFacts.Metadata.HAS_MATCHING_LOGINS) as Boolean?
if (hasMatchingLogins == true) {
AndroidAutofill.requestMatchingLogins.record(NoExtras())
} else {
AndroidAutofill.requestNoMatchingLogins.record(NoExtras())
}
}
Component.FEATURE_AUTOFILL to AutofillFacts.Items.AUTOFILL_SEARCH -> {
if (action == Action.SELECT) {
AndroidAutofill.searchItemSelected.record(NoExtras())
} else {
AndroidAutofill.searchDisplayed.record(NoExtras())
}
}
Component.FEATURE_AUTOFILL to AutofillFacts.Items.AUTOFILL_CONFIRMATION -> {
if (action == Action.CONFIRM) {
AndroidAutofill.confirmSuccessful.record(NoExtras())
} else {
AndroidAutofill.confirmCancelled.record(NoExtras())
}
}
Component.FEATURE_AUTOFILL to AutofillFacts.Items.AUTOFILL_LOCK -> {
if (action == Action.CONFIRM) {
AndroidAutofill.unlockSuccessful.record(NoExtras())
} else {
AndroidAutofill.unlockCancelled.record(NoExtras())
}
}
Component.FEATURE_SYNCEDTABS to SyncedTabsFacts.Items.SYNCED_TABS_SUGGESTION_CLICKED -> {
SyncedTabs.syncedTabsSuggestionClicked.record(NoExtras())
}
Component.FEATURE_AWESOMEBAR to AwesomeBarFacts.Items.BOOKMARK_SUGGESTION_CLICKED -> {
Awesomebar.bookmarkSuggestionClicked.record(NoExtras())
}
Component.FEATURE_AWESOMEBAR to AwesomeBarFacts.Items.CLIPBOARD_SUGGESTION_CLICKED -> {
Awesomebar.clipboardSuggestionClicked.record(NoExtras())
}
Component.FEATURE_AWESOMEBAR to AwesomeBarFacts.Items.HISTORY_SUGGESTION_CLICKED -> {
Awesomebar.historySuggestionClicked.record(NoExtras())
}
Component.FEATURE_AWESOMEBAR to AwesomeBarFacts.Items.SEARCH_ACTION_CLICKED -> {
Awesomebar.searchActionClicked.record(NoExtras())
}
Component.FEATURE_AWESOMEBAR to AwesomeBarFacts.Items.SEARCH_SUGGESTION_CLICKED -> {
Awesomebar.searchSuggestionClicked.record(NoExtras())
}
Component.FEATURE_AWESOMEBAR to AwesomeBarFacts.Items.OPENED_TAB_SUGGESTION_CLICKED -> {
Awesomebar.openedTabSuggestionClicked.record(NoExtras())
}
Component.FEATURE_CONTEXTMENU to ContextMenuFacts.Items.TEXT_SELECTION_OPTION -> {
when (metadata?.get("textSelectionOption")?.toString()) {
CONTEXT_MENU_COPY -> ContextualMenu.copyTapped.record(NoExtras())
CONTEXT_MENU_SEARCH,
CONTEXT_MENU_SEARCH_PRIVATELY,
-> ContextualMenu.searchTapped.record(NoExtras())
CONTEXT_MENU_SELECT_ALL -> ContextualMenu.selectAllTapped.record(NoExtras())
CONTEXT_MENU_SHARE -> ContextualMenu.shareTapped.record(NoExtras())
else -> Unit
}
}
Component.FEATURE_PWA to ProgressiveWebAppFacts.Items.HOMESCREEN_ICON_TAP -> {
ProgressiveWebApp.homescreenTap.record(NoExtras())
}
Component.FEATURE_PWA to ProgressiveWebAppFacts.Items.INSTALL_SHORTCUT -> {
ProgressiveWebApp.installTap.record(NoExtras())
}
Component.FEATURE_SEARCH to AdsTelemetry.SERP_ADD_CLICKED -> {
BrowserSearch.adClicks[value!!].add()
}
Component.FEATURE_SEARCH to AdsTelemetry.SERP_SHOWN_WITH_ADDS -> {
BrowserSearch.withAds[value!!].add()
}
Component.FEATURE_SEARCH to InContentTelemetry.IN_CONTENT_SEARCH -> {
BrowserSearch.inContent[value!!].add()
}
Component.SUPPORT_WEBEXTENSIONS to WebExtensionFacts.Items.WEB_EXTENSIONS_INITIALIZED -> {
metadata?.get("installed")?.let { installedAddons ->
if (installedAddons is List<*>) {
settings.installedAddonsCount = installedAddons.size
settings.installedAddonsList = installedAddons.joinToString(",")
}
}
metadata?.get("enabled")?.let { enabledAddons ->
if (enabledAddons is List<*>) {
settings.enabledAddonsCount = enabledAddons.size
settings.enabledAddonsList = enabledAddons.joinToString(",")
}
}
Unit
}
Component.COMPOSE_AWESOMEBAR to ComposeAwesomeBarFacts.Items.PROVIDER_DURATION -> {
metadata?.get(ComposeAwesomeBarFacts.MetadataKeys.DURATION_PAIR)
?.let { providerTiming ->
require(providerTiming is Pair<*, *>) { "Expected providerTiming to be a Pair" }
when (val provider = providerTiming.first as AwesomeBar.SuggestionProvider) {
is HistoryStorageSuggestionProvider -> PerfAwesomebar.historySuggestions
is BookmarksStorageSuggestionProvider -> PerfAwesomebar.bookmarkSuggestions
is SessionSuggestionProvider -> PerfAwesomebar.sessionSuggestions
is SearchSuggestionProvider -> PerfAwesomebar.searchEngineSuggestions
is ClipboardSuggestionProvider -> PerfAwesomebar.clipboardSuggestions
is ShortcutsSuggestionProvider -> PerfAwesomebar.shortcutsSuggestions
// NB: add PerfAwesomebar.syncedTabsSuggestions once we're using SyncedTabsSuggestionProvider
else -> {
Logger("Metrics").error("Unknown suggestion provider: $provider")
null
}
}?.accumulateSamples(listOf(providerTiming.second as Long))
}
Unit
}
Component.FEATURE_TOP_SITES to TopSitesFacts.Items.COUNT -> {
value?.let {
var count = 0
try {
count = it.toInt()
} catch (e: NumberFormatException) {
// Do nothing
}
settings.topSitesSize = count
}
Unit
}
Component.FEATURE_SITEPERMISSIONS to SitePermissionsFacts.Items.PERMISSIONS -> {
when (action) {
Action.DISPLAY -> SitePermissions.promptShown.record(
SitePermissions.PromptShownExtra(
value,
),
)
Action.CONFIRM ->
SitePermissions.permissionsAllowed.record(
SitePermissions.PermissionsAllowedExtra(
value,
),
)
Action.CANCEL ->
SitePermissions.permissionsDenied.record(
SitePermissions.PermissionsDeniedExtra(
value,
),
)
else -> {
// no-op
}
}
}
else -> {
// no-op
}
}
override fun start(type: MetricServiceType) {
val isEnabled = isTelemetryEnabled(type)
val isInitialized = isInitialized(type)
if (!isEnabled || isInitialized) {
return
}
services
.filter { it.type == type }
.forEach { it.start() }
initialized.add(type)
}
override fun stop(type: MetricServiceType) {
val isEnabled = isTelemetryEnabled(type)
val isInitialized = isInitialized(type)
if (isEnabled || !isInitialized) {
return
}
services
.filter { it.type == type }
.forEach { it.stop() }
initialized.remove(type)
}
override fun track(event: Event) {
services
.filter { it.shouldTrack(event) }
.forEach {
val isEnabled = isTelemetryEnabled(it.type)
val isInitialized = isInitialized(it.type)
if (!isEnabled || !isInitialized) {
return@forEach
}
it.track(event)
}
}
private fun isInitialized(type: MetricServiceType): Boolean = initialized.contains(type)
private fun isTelemetryEnabled(type: MetricServiceType): Boolean = when (type) {
MetricServiceType.Data -> isDataTelemetryEnabled()
MetricServiceType.Marketing -> isMarketingDataTelemetryEnabled()
}
companion object {
/**
* Text selection long press context items to be tracked.
*/
const val CONTEXT_MENU_COPY = "org.mozilla.geckoview.COPY"
const val CONTEXT_MENU_SEARCH = "CUSTOM_CONTEXT_MENU_SEARCH"
const val CONTEXT_MENU_SEARCH_PRIVATELY = "CUSTOM_CONTEXT_MENU_SEARCH_PRIVATELY"
const val CONTEXT_MENU_SELECT_ALL = "org.mozilla.geckoview.SELECT_ALL"
const val CONTEXT_MENU_SHARE = "CUSTOM_CONTEXT_MENU_SHARE"
/**
* Non - Text selection long press context menu items to be tracked.
*/
private val contextMenuAllowList = mapOf(
"mozac.feature.contextmenu.open_in_new_tab" to "open_in_new_tab",
"mozac.feature.contextmenu.open_in_private_tab" to "open_in_private_tab",
"mozac.feature.contextmenu.open_image_in_new_tab" to "open_image_in_new_tab",
"mozac.feature.contextmenu.save_image" to "save_image",
"mozac.feature.contextmenu.share_link" to "share_link",
"mozac.feature.contextmenu.copy_link" to "copy_link",
"mozac.feature.contextmenu.copy_image_location" to "copy_image_location",
"mozac.feature.contextmenu.share_image" to "share_image",
)
}
}