/* 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.MainScope import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking import mozilla.components.service.glean.BuildConfig import mozilla.components.service.glean.Glean import mozilla.components.service.glean.config.Configuration import mozilla.components.service.glean.private.NoExtraKeys import mozilla.components.support.utils.Browsers 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.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.Metrics import org.mozilla.fenix.GleanMetrics.Pings import org.mozilla.fenix.GleanMetrics.PrivateBrowsingMode import org.mozilla.fenix.GleanMetrics.PrivateBrowsingShortcut import org.mozilla.fenix.GleanMetrics.QrScanner import org.mozilla.fenix.GleanMetrics.QuickActionSheet import org.mozilla.fenix.GleanMetrics.ReaderMode import org.mozilla.fenix.GleanMetrics.SearchDefaultEngine import org.mozilla.fenix.GleanMetrics.SearchShortcuts 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.TrackingProtection import org.mozilla.fenix.ext.components private class EventWrapper>( private val recorder: ((Map?) -> Unit), private val keyMapper: ((String) -> T)? = null ) { private val String.asCamelCase: String get() = this.split("_").reduceIndexed { index, acc, s -> if (index == 0) acc + s else acc + s.capitalize() } fun track(event: Event) { val extras = if (keyMapper != null) { event.extras?.mapKeys { keyMapper.invoke(it.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.SearchShortcutMenuOpened -> EventWrapper( { SearchShortcuts.opened.record(it) } ) is Event.SearchShortcutMenuClosed -> EventWrapper( { SearchShortcuts.closed.record(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.FindInPageNext -> EventWrapper( { FindInPage.nextResult.record(it) } ) is Event.FindInPagePrevious -> EventWrapper( { FindInPage.previousResult.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.QuickActionSheetOpened -> EventWrapper( { QuickActionSheet.opened.record(it) } ) is Event.QuickActionSheetClosed -> EventWrapper( { QuickActionSheet.closed.record(it) } ) is Event.QuickActionSheetShareTapped -> EventWrapper( { QuickActionSheet.shareTapped.record(it) } ) is Event.QuickActionSheetBookmarkTapped -> EventWrapper( { QuickActionSheet.bookmarkTapped.record(it) } ) is Event.QuickActionSheetDownloadTapped -> EventWrapper( { QuickActionSheet.downloadTapped.record(it) } ) is Event.QuickActionSheetOpenInAppTapped -> EventWrapper( { QuickActionSheet.openAppTapped.record(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) }, { Events.whatsNewTappedKeys.valueOf(it) } ) is Event.TabMediaPlay -> EventWrapper( { Tab.mediaPlay.record(it) } ) is Event.TabMediaPause -> EventWrapper( { Tab.mediaPause.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) } ) // 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 } class GleanMetricsService(private val context: Context) : MetricsService { 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 starter: Job private val activationPing = ActivationPing(context) override fun start() { Glean.setUploadEnabled(true) if (initialized) return initialized = true starter = MainScope().launch { Glean.registerPings(Pings) Glean.initialize(context, Configuration(channel = BuildConfig.BUILD_TYPE)) setStartupMetrics() } } internal fun setStartupMetrics() { Metrics.apply { defaultBrowser.set(Browsers.all(context).isDefaultBrowser) MozillaProductDetector.getMozillaBrowserDefault(context)?.also { defaultMozBrowser.set(it) } mozillaProducts.set(MozillaProductDetector.getInstalledMozillaProducts(context)) } 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() } override fun stop() { /* * We cannot stop until we're done starting. */ runBlocking { starter.join(); } Glean.setUploadEnabled(false) } override fun track(event: Event) { event.wrapper?.track(event) } override fun shouldTrack(event: Event): Boolean { return event.wrapper != null } }