/* 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 android.content.Context import kotlinx.coroutines.Job import kotlinx.coroutines.launch import kotlinx.coroutines.MainScope import mozilla.components.service.glean.Glean import mozilla.components.service.glean.private.NoExtraKeys import mozilla.components.support.base.log.logger.Logger import org.mozilla.fenix.GleanMetrics.AboutPage import org.mozilla.fenix.GleanMetrics.AppTheme import org.mozilla.fenix.GleanMetrics.BookmarksManagement import org.mozilla.fenix.GleanMetrics.Collections import org.mozilla.fenix.GleanMetrics.ContextMenu import org.mozilla.fenix.GleanMetrics.CrashReporter import org.mozilla.fenix.GleanMetrics.CustomTab import org.mozilla.fenix.GleanMetrics.DownloadNotification import org.mozilla.fenix.GleanMetrics.ErrorPage import org.mozilla.fenix.GleanMetrics.Events import org.mozilla.fenix.GleanMetrics.FindInPage import org.mozilla.fenix.GleanMetrics.History import org.mozilla.fenix.GleanMetrics.Library import org.mozilla.fenix.GleanMetrics.Logins import org.mozilla.fenix.GleanMetrics.MediaNotification import org.mozilla.fenix.GleanMetrics.MediaState import org.mozilla.fenix.GleanMetrics.Metrics import org.mozilla.fenix.GleanMetrics.Pings import org.mozilla.fenix.GleanMetrics.Pocket import org.mozilla.fenix.GleanMetrics.PrivateBrowsingMode import org.mozilla.fenix.GleanMetrics.PrivateBrowsingShortcut import org.mozilla.fenix.GleanMetrics.QrScanner import org.mozilla.fenix.GleanMetrics.ReaderMode import org.mozilla.fenix.GleanMetrics.SearchDefaultEngine import org.mozilla.fenix.GleanMetrics.SearchShortcuts import org.mozilla.fenix.GleanMetrics.SearchSuggestions import org.mozilla.fenix.GleanMetrics.SearchWidget import org.mozilla.fenix.GleanMetrics.SyncAccount import org.mozilla.fenix.GleanMetrics.SyncAuth import org.mozilla.fenix.GleanMetrics.Tab import org.mozilla.fenix.GleanMetrics.ToolbarSettings import org.mozilla.fenix.GleanMetrics.TopSites import org.mozilla.fenix.GleanMetrics.TrackingProtection import org.mozilla.fenix.GleanMetrics.UserSpecifiedSearchEngines import org.mozilla.fenix.ext.components import org.mozilla.fenix.ext.settings import org.mozilla.fenix.utils.BrowsersCache private class EventWrapper>( private val recorder: ((Map?) -> Unit), private val keyMapper: ((String) -> T)? = null ) { /** * Converts snake_case string to camelCase. */ private fun String.asCamelCase(): String { val parts = split("_") val builder = StringBuilder() for ((index, part) in parts.withIndex()) { if (index == 0) { builder.append(part) } else { builder.append(part[0].toUpperCase()) builder.append(part.substring(1)) } } return builder.toString() } fun track(event: Event) { val extras = if (keyMapper != null) { event.extras?.mapKeys { (key) -> keyMapper.invoke(key.toString().asCamelCase()) } } else { null } this.recorder(extras) } } private val Event.wrapper: EventWrapper<*>? get() = when (this) { is Event.OpenedApp -> EventWrapper( { Events.appOpened.record(it) }, { Events.appOpenedKeys.valueOf(it) } ) is Event.SearchBarTapped -> EventWrapper( { Events.searchBarTapped.record(it) }, { Events.searchBarTappedKeys.valueOf(it) } ) is Event.EnteredUrl -> EventWrapper( { Events.enteredUrl.record(it) }, { Events.enteredUrlKeys.valueOf(it) } ) is Event.PerformedSearch -> EventWrapper( { Metrics.searchCount[this.eventSource.countLabel].add(1) Events.performedSearch.record(it) }, { Events.performedSearchKeys.valueOf(it) } ) is Event.SearchShortcutSelected -> EventWrapper( { SearchShortcuts.selected.record(it) }, { SearchShortcuts.selectedKeys.valueOf(it) } ) is Event.ReaderModeAvailable -> EventWrapper( { ReaderMode.available.record(it) } ) is Event.FindInPageOpened -> EventWrapper( { FindInPage.opened.record(it) } ) is Event.FindInPageClosed -> EventWrapper( { FindInPage.closed.record(it) } ) is Event.FindInPageSearchCommitted -> EventWrapper( { FindInPage.searchedPage.record(it) } ) is Event.ContextMenuItemTapped -> EventWrapper( { ContextMenu.itemTapped.record(it) }, { ContextMenu.itemTappedKeys.valueOf(it) } ) is Event.CrashReporterOpened -> EventWrapper( { CrashReporter.opened.record(it) } ) is Event.CrashReporterClosed -> EventWrapper( { CrashReporter.closed.record(it) }, { CrashReporter.closedKeys.valueOf(it) } ) is Event.BrowserMenuItemTapped -> EventWrapper( { Events.browserMenuAction.record(it) }, { Events.browserMenuActionKeys.valueOf(it) } ) is Event.OpenedBookmarkInNewTab -> EventWrapper( { BookmarksManagement.openInNewTab.record(it) } ) is Event.OpenedBookmarksInNewTabs -> EventWrapper( { BookmarksManagement.openInNewTabs.record(it) } ) is Event.OpenedBookmarkInPrivateTab -> EventWrapper( { BookmarksManagement.openInPrivateTab.record(it) } ) is Event.OpenedBookmarksInPrivateTabs -> EventWrapper( { BookmarksManagement.openInPrivateTabs.record(it) } ) is Event.EditedBookmark -> EventWrapper( { BookmarksManagement.edited.record(it) } ) is Event.MovedBookmark -> EventWrapper( { BookmarksManagement.moved.record(it) } ) is Event.RemoveBookmark -> EventWrapper( { BookmarksManagement.removed.record(it) } ) is Event.RemoveBookmarks -> EventWrapper( { BookmarksManagement.multiRemoved.record(it) } ) is Event.ShareBookmark -> EventWrapper( { BookmarksManagement.shared.record(it) } ) is Event.CopyBookmark -> EventWrapper( { BookmarksManagement.copied.record(it) } ) is Event.AddBookmarkFolder -> EventWrapper( { BookmarksManagement.folderAdd.record(it) } ) is Event.RemoveBookmarkFolder -> EventWrapper( { BookmarksManagement.folderRemove.record(it) } ) is Event.CustomTabsMenuOpened -> EventWrapper( { CustomTab.menu.record(it) } ) is Event.CustomTabsActionTapped -> EventWrapper( { CustomTab.actionButton.record(it) } ) is Event.CustomTabsClosed -> EventWrapper( { CustomTab.closed.record(it) } ) is Event.UriOpened -> EventWrapper( { Events.totalUriCount.add(1) } ) is Event.QRScannerOpened -> EventWrapper( { QrScanner.opened.record(it) } ) is Event.QRScannerPromptDisplayed -> EventWrapper( { QrScanner.promptDisplayed.record(it) } ) is Event.QRScannerNavigationAllowed -> EventWrapper( { QrScanner.navigationAllowed.record(it) } ) is Event.QRScannerNavigationDenied -> EventWrapper( { QrScanner.navigationDenied.record(it) } ) is Event.LibraryOpened -> EventWrapper( { Library.opened.record(it) } ) is Event.LibraryClosed -> EventWrapper( { Library.closed.record(it) } ) is Event.LibrarySelectedItem -> EventWrapper( { Library.selectedItem.record(it) }, { Library.selectedItemKeys.valueOf(it) } ) is Event.ErrorPageVisited -> EventWrapper( { ErrorPage.visitedError.record(it) }, { ErrorPage.visitedErrorKeys.valueOf(it) } ) is Event.SyncAuthOpened -> EventWrapper( { SyncAuth.opened.record(it) } ) is Event.SyncAuthClosed -> EventWrapper( { SyncAuth.closed.record(it) } ) is Event.SyncAuthSignIn -> EventWrapper( { SyncAuth.signIn.record(it) } ) is Event.SyncAuthSignUp -> EventWrapper( { SyncAuth.signUp.record(it) } ) is Event.SyncAuthPaired -> EventWrapper( { SyncAuth.paired.record(it) } ) is Event.SyncAuthOtherExternal -> EventWrapper( { SyncAuth.otherExternal.record(it) } ) is Event.SyncAuthFromShared -> EventWrapper( { SyncAuth.autoLogin.record(it) } ) is Event.SyncAuthRecovered -> EventWrapper( { SyncAuth.recovered.record(it) } ) is Event.SyncAuthSignOut -> EventWrapper( { SyncAuth.signOut.record(it) } ) is Event.SyncAuthScanPairing -> EventWrapper( { SyncAuth.scanPairing.record(it) } ) is Event.SyncAccountOpened -> EventWrapper( { SyncAccount.opened.record(it) } ) is Event.SyncAccountClosed -> EventWrapper( { SyncAccount.closed.record(it) } ) is Event.SyncAccountSyncNow -> EventWrapper( { SyncAccount.syncNow.record(it) } ) is Event.SignInToSendTab -> EventWrapper( { SyncAccount.signInToSendTab.record(it) } ) is Event.SendTab -> EventWrapper( { SyncAccount.sendTab.record(it) } ) is Event.PreferenceToggled -> EventWrapper( { Events.preferenceToggled.record(it) }, { Events.preferenceToggledKeys.valueOf(it) } ) is Event.HistoryOpened -> EventWrapper( { History.opened.record(it) } ) is Event.HistoryItemShared -> EventWrapper( { History.shared.record(it) } ) is Event.HistoryItemOpened -> EventWrapper( { History.openedItem.record(it) } ) is Event.HistoryItemRemoved -> EventWrapper( { History.removed.record(it) } ) is Event.HistoryAllItemsRemoved -> EventWrapper( { History.removedAll.record(it) } ) is Event.CollectionRenamed -> EventWrapper( { Collections.renamed.record(it) } ) is Event.CollectionTabRestored -> EventWrapper( { Collections.tabRestored.record(it) } ) is Event.CollectionAllTabsRestored -> EventWrapper( { Collections.allTabsRestored.record(it) } ) is Event.CollectionTabRemoved -> EventWrapper( { Collections.tabRemoved.record(it) } ) is Event.CollectionShared -> EventWrapper( { Collections.shared.record(it) } ) is Event.CollectionRemoved -> EventWrapper( { Collections.removed.record(it) } ) is Event.CollectionTabSelectOpened -> EventWrapper( { Collections.tabSelectOpened.record(it) } ) is Event.ReaderModeOpened -> EventWrapper( { ReaderMode.opened.record(it) } ) is Event.ReaderModeAppearanceOpened -> EventWrapper( { ReaderMode.appearance.record(it) } ) is Event.CollectionTabLongPressed -> EventWrapper( { Collections.longPress.record(it) } ) is Event.CollectionSaveButtonPressed -> EventWrapper( { Collections.saveButton.record(it) }, { Collections.saveButtonKeys.valueOf(it) } ) is Event.CollectionAddTabPressed -> EventWrapper( { Collections.addTabButton.record(it) } ) is Event.CollectionRenamePressed -> EventWrapper( { Collections.renameButton.record(it) } ) is Event.CollectionSaved -> EventWrapper( { Collections.saved.record(it) }, { Collections.savedKeys.valueOf(it) } ) is Event.CollectionTabsAdded -> EventWrapper( { Collections.tabsAdded.record(it) }, { Collections.tabsAddedKeys.valueOf(it) } ) is Event.SearchWidgetNewTabPressed -> EventWrapper( { SearchWidget.newTabButton.record(it) } ) is Event.SearchWidgetVoiceSearchPressed -> EventWrapper( { SearchWidget.voiceButton.record(it) } ) is Event.PrivateBrowsingGarbageIconTapped -> EventWrapper( { PrivateBrowsingMode.garbageIcon.record(it) } ) is Event.PrivateBrowsingSnackbarUndoTapped -> EventWrapper( { PrivateBrowsingMode.snackbarUndo.record(it) } ) is Event.PrivateBrowsingNotificationTapped -> EventWrapper( { PrivateBrowsingMode.notificationTapped.record(it) } ) is Event.PrivateBrowsingNotificationOpenTapped -> EventWrapper( { PrivateBrowsingMode.notificationOpen.record(it) } ) is Event.PrivateBrowsingNotificationDeleteAndOpenTapped -> EventWrapper( { PrivateBrowsingMode.notificationDelete.record(it) } ) is Event.PrivateBrowsingCreateShortcut -> EventWrapper( { PrivateBrowsingShortcut.createShortcut.record(it) } ) is Event.PrivateBrowsingAddShortcutCFR -> EventWrapper( { PrivateBrowsingShortcut.cfrAddShortcut.record(it) } ) is Event.PrivateBrowsingCancelCFR -> EventWrapper( { PrivateBrowsingShortcut.cfrCancel.record(it) } ) is Event.PrivateBrowsingPinnedShortcutPrivateTab -> EventWrapper( { PrivateBrowsingShortcut.pinnedShortcutPriv.record(it) } ) is Event.PrivateBrowsingStaticShortcutTab -> EventWrapper( { PrivateBrowsingShortcut.staticShortcutTab.record(it) } ) is Event.PrivateBrowsingStaticShortcutPrivateTab -> EventWrapper( { PrivateBrowsingShortcut.staticShortcutPriv.record(it) } ) is Event.WhatsNewTapped -> EventWrapper( { Events.whatsNewTapped.record(it) } ) is Event.TabMediaPlay -> EventWrapper( { Tab.mediaPlay.record(it) } ) is Event.TabMediaPause -> EventWrapper( { Tab.mediaPause.record(it) } ) is Event.MediaPlayState -> EventWrapper( { MediaState.play.record(it) } ) is Event.MediaPauseState -> EventWrapper( { MediaState.pause.record(it) } ) is Event.MediaStopState -> EventWrapper( { MediaState.stop.record(it) } ) is Event.InAppNotificationDownloadOpen -> EventWrapper( { DownloadNotification.inAppOpen.record(it) } ) is Event.InAppNotificationDownloadTryAgain -> EventWrapper( { DownloadNotification.inAppTryAgain.record(it) } ) is Event.NotificationDownloadCancel -> EventWrapper( { DownloadNotification.cancel.record(it) } ) is Event.NotificationDownloadOpen -> EventWrapper( { DownloadNotification.open.record(it) } ) is Event.NotificationDownloadPause -> EventWrapper( { DownloadNotification.pause.record(it) } ) is Event.NotificationDownloadResume -> EventWrapper( { DownloadNotification.resume.record(it) } ) is Event.NotificationDownloadTryAgain -> EventWrapper( { DownloadNotification.tryAgain.record(it) } ) is Event.NotificationMediaPlay -> EventWrapper( { MediaNotification.play.record(it) } ) is Event.NotificationMediaPause -> EventWrapper( { MediaNotification.pause.record(it) } ) is Event.TrackingProtectionTrackerList -> EventWrapper( { TrackingProtection.etpTrackerList.record(it) } ) is Event.TrackingProtectionIconPressed -> EventWrapper( { TrackingProtection.etpShield.record(it) } ) is Event.TrackingProtectionSettingsPanel -> EventWrapper( { TrackingProtection.panelSettings.record(it) } ) is Event.TrackingProtectionSettings -> EventWrapper( { TrackingProtection.etpSettings.record(it) } ) is Event.TrackingProtectionException -> EventWrapper( { TrackingProtection.exceptionAdded.record(it) } ) is Event.TrackingProtectionSettingChanged -> EventWrapper( { TrackingProtection.etpSettingChanged.record(it) }, { TrackingProtection.etpSettingChangedKeys.valueOf(it) } ) is Event.OpenedLink -> EventWrapper( { Events.openedLink.record(it) }, { Events.openedLinkKeys.valueOf(it) } ) is Event.OpenLogins -> EventWrapper( { Logins.openLogins.record(it) } ) is Event.OpenOneLogin -> EventWrapper( { Logins.openIndividualLogin.record(it) } ) is Event.CopyLogin -> EventWrapper( { Logins.copyLogin.record(it) } ) is Event.ViewLoginPassword -> EventWrapper( { Logins.viewPasswordLogin.record(it) } ) is Event.PrivateBrowsingShowSearchSuggestions -> EventWrapper( { SearchSuggestions.enableInPrivate.record(it) } ) is Event.ToolbarPositionChanged -> EventWrapper( { ToolbarSettings.changedPosition.record(it) }, { ToolbarSettings.changedPositionKeys.valueOf(it) } ) is Event.CustomEngineAdded -> EventWrapper( { UserSpecifiedSearchEngines.customEngineAdded.record(it) } ) is Event.CustomEngineDeleted -> EventWrapper( { UserSpecifiedSearchEngines.customEngineDeleted.record(it) } ) is Event.SaveLoginsSettingChanged -> EventWrapper( { Logins.saveLoginsSettingChanged.record(it) }, { Logins.saveLoginsSettingChangedKeys.valueOf(it) } ) is Event.TopSiteOpenInNewTab -> EventWrapper( { TopSites.openInNewTab.record(it) } ) is Event.TopSiteOpenInPrivateTab -> EventWrapper( { TopSites.openInPrivateTab.record(it) } ) is Event.TopSiteRemoved -> EventWrapper( { TopSites.remove.record(it) } ) is Event.SupportTapped -> EventWrapper( { AboutPage.supportTapped.record(it) } ) is Event.PrivacyNoticeTapped -> EventWrapper( { AboutPage.privacyNoticeTapped.record(it) } ) is Event.RightsTapped -> EventWrapper( { AboutPage.rightsTapped.record(it) } ) is Event.LicensingTapped -> EventWrapper( { AboutPage.licensingTapped.record(it) } ) is Event.LibrariesThatWeUseTapped -> EventWrapper( { AboutPage.librariesTapped.record(it) } ) is Event.PocketTopSiteClicked -> EventWrapper( { Pocket.pocketTopSiteClicked.record(it) } ) is Event.PocketTopSiteRemoved -> EventWrapper( { Pocket.pocketTopSiteRemoved.record(it) } ) is Event.DarkThemeSelected -> EventWrapper( { AppTheme.darkThemeSelected.record(it) }, { AppTheme.darkThemeSelectedKeys.valueOf(it) } ) // Don't record other events in Glean: is Event.AddBookmark -> null is Event.OpenedBookmark -> null is Event.OpenedAppFirstRun -> null is Event.InteractWithSearchURLArea -> null is Event.ClearedPrivateData -> null is Event.DismissedOnboarding -> null is Event.FennecToFenixMigrated -> null } class GleanMetricsService(private val context: Context) : MetricsService { override val type = MetricServiceType.Data private val logger = Logger("GleanMetricsService") private var initialized = false /* * We need to keep an eye on when we are done starting so that we don't * accidentally stop ourselves before we've ever started. */ private lateinit var gleanInitializer: Job private lateinit var gleanSetStartupMetrics: Job private val activationPing = ActivationPing(context) override fun start() { logger.debug("Enabling Glean.") // Initialization of Glean already happened in FenixApplication. Glean.setUploadEnabled(true) if (initialized) return initialized = true // We have to initialize Glean *on* the main thread, because it registers lifecycle // observers. However, the activation ping must be sent *off* of the main thread, // because it calls Google ad APIs that must be called *off* of the main thread. // These two things actually happen in parallel, but that should be ok because Glean // can handle events being recorded before it's initialized. gleanInitializer = MainScope().launch { Glean.registerPings(Pings) } // setStartupMetrics is not a fast function. It does not need to be done before we can consider // ourselves initialized. So, let's do it, well, later. gleanSetStartupMetrics = MainScope().launch { setStartupMetrics() } } internal fun setStartupMetrics() { Metrics.apply { defaultBrowser.set(BrowsersCache.all(context).isDefaultBrowser) MozillaProductDetector.getMozillaBrowserDefault(context)?.also { defaultMozBrowser.set(it) } mozillaProducts.set(MozillaProductDetector.getInstalledMozillaProducts(context)) adjustCampaign.set(context.settings().adjustCampaignId) toolbarPosition.set( if (context.settings().shouldUseBottomToolbar) { Event.ToolbarPositionChanged.Position.BOTTOM.name } else { Event.ToolbarPositionChanged.Position.TOP.name } ) } SearchDefaultEngine.apply { val defaultEngine = context .components .search .searchEngineManager .defaultSearchEngine ?: return@apply code.set(defaultEngine.identifier) name.set(defaultEngine.name) submissionUrl.set(defaultEngine.buildSearchUrl("")) } activationPing.checkAndSend() InstallationPing(context).checkAndSend() } override fun stop() { gleanInitializer.cancel() gleanSetStartupMetrics.cancel() Glean.setUploadEnabled(false) } override fun track(event: Event) { event.wrapper?.track(event) } override fun shouldTrack(event: Event): Boolean { return event.wrapper != null } }