diff --git a/app/src/main/java/org/mozilla/fenix/components/metrics/MetricController.kt b/app/src/main/java/org/mozilla/fenix/components/metrics/MetricController.kt new file mode 100644 index 000000000..3b7c5bb5c --- /dev/null +++ b/app/src/main/java/org/mozilla/fenix/components/metrics/MetricController.kt @@ -0,0 +1,227 @@ +/* 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.awesomebar.facts.BrowserAwesomeBarFacts +import mozilla.components.browser.menu.facts.BrowserMenuFacts +import mozilla.components.browser.toolbar.facts.ToolbarFacts +import mozilla.components.concept.awesomebar.AwesomeBar +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.customtabs.CustomTabsFacts +import mozilla.components.feature.downloads.facts.DownloadsFacts +import mozilla.components.feature.findinpage.facts.FindInPageFacts +import mozilla.components.feature.media.facts.MediaFacts +import mozilla.components.feature.prompts.dialog.LoginDialogFacts +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 org.mozilla.fenix.BuildConfig +import org.mozilla.fenix.GleanMetrics.Addons +import org.mozilla.fenix.GleanMetrics.PerfAwesomebar +import org.mozilla.fenix.search.awesomebar.ShortcutsSuggestionProvider + +interface MetricController { + fun start(type: MetricServiceType) + fun stop(type: MetricServiceType) + fun track(event: Event) + + companion object { + fun create( + services: List, + isDataTelemetryEnabled: () -> Boolean, + isMarketingDataTelemetryEnabled: () -> Boolean + ): MetricController { + return if (BuildConfig.TELEMETRY) { + ReleaseMetricController( + services, + isDataTelemetryEnabled, + isMarketingDataTelemetryEnabled + ) + } 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 +internal class ReleaseMetricController( + private val services: List, + private val isDataTelemetryEnabled: () -> Boolean, + private val isMarketingDataTelemetryEnabled: () -> Boolean +) : MetricController { + private var initialized = mutableSetOf() + + init { + Facts.registerProcessor(object : FactProcessor { + override fun process(fact: Fact) { + fact.toEvent()?.also { + track(it) + } + } + }) + } + + 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() + } + + private fun Fact.toEvent(): Event? = when (Pair(component, item)) { + Component.FEATURE_PROMPTS to LoginDialogFacts.Items.DISPLAY -> Event.LoginDialogPromptDisplayed + Component.FEATURE_PROMPTS to LoginDialogFacts.Items.CANCEL -> Event.LoginDialogPromptCancelled + Component.FEATURE_PROMPTS to LoginDialogFacts.Items.NEVER_SAVE -> Event.LoginDialogPromptNeverSave + Component.FEATURE_PROMPTS to LoginDialogFacts.Items.SAVE -> Event.LoginDialogPromptSave + + Component.FEATURE_FINDINPAGE to FindInPageFacts.Items.CLOSE -> Event.FindInPageClosed + Component.FEATURE_FINDINPAGE to FindInPageFacts.Items.INPUT -> Event.FindInPageSearchCommitted + Component.FEATURE_CONTEXTMENU to ContextMenuFacts.Items.ITEM -> { + metadata?.get("item")?.let { Event.ContextMenuItemTapped.create(it.toString()) } + } + + Component.BROWSER_TOOLBAR to ToolbarFacts.Items.MENU -> { + metadata?.get("customTab")?.let { Event.CustomTabsMenuOpened } + } + Component.BROWSER_MENU to BrowserMenuFacts.Items.WEB_EXTENSION_MENU_ITEM -> { + metadata?.get("id")?.let { Event.AddonsOpenInToolbarMenu(it.toString()) } + } + Component.FEATURE_CUSTOMTABS to CustomTabsFacts.Items.CLOSE -> Event.CustomTabsClosed + Component.FEATURE_CUSTOMTABS to CustomTabsFacts.Items.ACTION_BUTTON -> Event.CustomTabsActionTapped + + Component.FEATURE_DOWNLOADS to DownloadsFacts.Items.NOTIFICATION -> { + when (action) { + Action.CANCEL -> Event.NotificationDownloadCancel + Action.OPEN -> Event.NotificationDownloadOpen + Action.PAUSE -> Event.NotificationDownloadPause + Action.RESUME -> Event.NotificationDownloadResume + Action.TRY_AGAIN -> Event.NotificationDownloadTryAgain + else -> null + } + } + + Component.FEATURE_MEDIA to MediaFacts.Items.NOTIFICATION -> { + when (action) { + Action.PLAY -> Event.NotificationMediaPlay + Action.PAUSE -> Event.NotificationMediaPause + else -> null + } + } + Component.FEATURE_MEDIA to MediaFacts.Items.STATE -> { + when (action) { + Action.PLAY -> Event.MediaPlayState + Action.PAUSE -> Event.MediaPauseState + Action.STOP -> Event.MediaStopState + else -> null + } + } + Component.SUPPORT_WEBEXTENSIONS to WebExtensionFacts.Items.WEB_EXTENSIONS_INITIALIZED -> { + metadata?.get("installed")?.let { installedAddons -> + if (installedAddons is List<*>) { + Addons.installedAddons.set(installedAddons.map { it.toString() }) + Addons.hasInstalledAddons.set(installedAddons.size > 0) + } + } + + metadata?.get("enabled")?.let { enabledAddons -> + if (enabledAddons is List<*>) { + Addons.enabledAddons.set(enabledAddons.map { it.toString() }) + Addons.hasEnabledAddons.set(enabledAddons.size > 0) + } + } + + null + } + Component.BROWSER_AWESOMEBAR to BrowserAwesomeBarFacts.Items.PROVIDER_DURATION -> { + metadata?.get(BrowserAwesomeBarFacts.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(longArrayOf(providerTiming.second as Long)) + } + null + } + else -> null + } +} diff --git a/app/src/main/java/org/mozilla/fenix/components/metrics/Metrics.kt b/app/src/main/java/org/mozilla/fenix/components/metrics/Metrics.kt index 93ce11cc7..26a408340 100644 --- a/app/src/main/java/org/mozilla/fenix/components/metrics/Metrics.kt +++ b/app/src/main/java/org/mozilla/fenix/components/metrics/Metrics.kt @@ -5,32 +5,8 @@ package org.mozilla.fenix.components.metrics import android.content.Context -import androidx.annotation.VisibleForTesting -import mozilla.components.browser.awesomebar.facts.BrowserAwesomeBarFacts import mozilla.components.browser.errorpages.ErrorType -import mozilla.components.browser.menu.facts.BrowserMenuFacts import mozilla.components.browser.search.SearchEngine -import mozilla.components.browser.toolbar.facts.ToolbarFacts -import mozilla.components.concept.awesomebar.AwesomeBar -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.customtabs.CustomTabsFacts -import mozilla.components.feature.downloads.facts.DownloadsFacts -import mozilla.components.feature.findinpage.facts.FindInPageFacts -import mozilla.components.feature.media.facts.MediaFacts -import mozilla.components.feature.prompts.dialog.LoginDialogFacts -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 org.mozilla.fenix.BuildConfig import org.mozilla.fenix.GleanMetrics.Addons import org.mozilla.fenix.GleanMetrics.AppTheme import org.mozilla.fenix.GleanMetrics.Autoplay @@ -41,13 +17,11 @@ import org.mozilla.fenix.GleanMetrics.ErrorPage import org.mozilla.fenix.GleanMetrics.Events import org.mozilla.fenix.GleanMetrics.Logins import org.mozilla.fenix.GleanMetrics.Onboarding -import org.mozilla.fenix.GleanMetrics.PerfAwesomebar import org.mozilla.fenix.GleanMetrics.SearchShortcuts import org.mozilla.fenix.GleanMetrics.Tip import org.mozilla.fenix.GleanMetrics.ToolbarSettings import org.mozilla.fenix.GleanMetrics.TrackingProtection import org.mozilla.fenix.R -import org.mozilla.fenix.search.awesomebar.ShortcutsSuggestionProvider import java.util.Locale sealed class Event { @@ -532,92 +506,6 @@ sealed class Event { get() = null } -private fun Fact.toEvent(): Event? = when (Pair(component, item)) { - Component.FEATURE_PROMPTS to LoginDialogFacts.Items.DISPLAY -> Event.LoginDialogPromptDisplayed - Component.FEATURE_PROMPTS to LoginDialogFacts.Items.CANCEL -> Event.LoginDialogPromptCancelled - Component.FEATURE_PROMPTS to LoginDialogFacts.Items.NEVER_SAVE -> Event.LoginDialogPromptNeverSave - Component.FEATURE_PROMPTS to LoginDialogFacts.Items.SAVE -> Event.LoginDialogPromptSave - - Component.FEATURE_FINDINPAGE to FindInPageFacts.Items.CLOSE -> Event.FindInPageClosed - Component.FEATURE_FINDINPAGE to FindInPageFacts.Items.INPUT -> Event.FindInPageSearchCommitted - Component.FEATURE_CONTEXTMENU to ContextMenuFacts.Items.ITEM -> { - metadata?.get("item")?.let { Event.ContextMenuItemTapped.create(it.toString()) } - } - - Component.BROWSER_TOOLBAR to ToolbarFacts.Items.MENU -> { - metadata?.get("customTab")?.let { Event.CustomTabsMenuOpened } - } - Component.BROWSER_MENU to BrowserMenuFacts.Items.WEB_EXTENSION_MENU_ITEM -> { - metadata?.get("id")?.let { Event.AddonsOpenInToolbarMenu(it.toString()) } - } - Component.FEATURE_CUSTOMTABS to CustomTabsFacts.Items.CLOSE -> Event.CustomTabsClosed - Component.FEATURE_CUSTOMTABS to CustomTabsFacts.Items.ACTION_BUTTON -> Event.CustomTabsActionTapped - - Component.FEATURE_DOWNLOADS to DownloadsFacts.Items.NOTIFICATION -> { - when (action) { - Action.CANCEL -> Event.NotificationDownloadCancel - Action.OPEN -> Event.NotificationDownloadOpen - Action.PAUSE -> Event.NotificationDownloadPause - Action.RESUME -> Event.NotificationDownloadResume - Action.TRY_AGAIN -> Event.NotificationDownloadTryAgain - else -> null - } - } - - Component.FEATURE_MEDIA to MediaFacts.Items.NOTIFICATION -> { - when (action) { - Action.PLAY -> Event.NotificationMediaPlay - Action.PAUSE -> Event.NotificationMediaPause - else -> null - } - } - Component.FEATURE_MEDIA to MediaFacts.Items.STATE -> { - when (action) { - Action.PLAY -> Event.MediaPlayState - Action.PAUSE -> Event.MediaPauseState - Action.STOP -> Event.MediaStopState - else -> null - } - } - Component.SUPPORT_WEBEXTENSIONS to WebExtensionFacts.Items.WEB_EXTENSIONS_INITIALIZED -> { - metadata?.get("installed")?.let { installedAddons -> - if (installedAddons is List<*>) { - Addons.installedAddons.set(installedAddons.map { it.toString() }) - Addons.hasInstalledAddons.set(installedAddons.size > 0) - } - } - - metadata?.get("enabled")?.let { enabledAddons -> - if (enabledAddons is List<*>) { - Addons.enabledAddons.set(enabledAddons.map { it.toString() }) - Addons.hasEnabledAddons.set(enabledAddons.size > 0) - } - } - - null - } - Component.BROWSER_AWESOMEBAR to BrowserAwesomeBarFacts.Items.PROVIDER_DURATION -> { - metadata?.get(BrowserAwesomeBarFacts.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(longArrayOf(providerTiming.second as Long)) - } - null - } - else -> null -} - enum class MetricServiceType { Data, Marketing; } @@ -630,111 +518,3 @@ interface MetricsService { fun track(event: Event) fun shouldTrack(event: Event): Boolean } - -interface MetricController { - fun start(type: MetricServiceType) - fun stop(type: MetricServiceType) - fun track(event: Event) - - companion object { - fun create( - services: List, - isDataTelemetryEnabled: () -> Boolean, - isMarketingDataTelemetryEnabled: () -> Boolean - ): MetricController { - return if (BuildConfig.TELEMETRY) { - ReleaseMetricController( - services, - isDataTelemetryEnabled, - isMarketingDataTelemetryEnabled - ) - } 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 -internal class ReleaseMetricController( - private val services: List, - private val isDataTelemetryEnabled: () -> Boolean, - private val isMarketingDataTelemetryEnabled: () -> Boolean -) : MetricController { - private var initialized = mutableSetOf() - - init { - Facts.registerProcessor(object : FactProcessor { - override fun process(fact: Fact) { - fact.toEvent()?.also { - track(it) - } - } - }) - } - - 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() - } -}