/* 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 mozilla.components.service.fxa.manager.SyncEnginesStorage 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.Addons import org.mozilla.fenix.GleanMetrics.AppTheme import org.mozilla.fenix.GleanMetrics.Autoplay import org.mozilla.fenix.GleanMetrics.BookmarksManagement import org.mozilla.fenix.GleanMetrics.BrowserSearch import org.mozilla.fenix.GleanMetrics.Collections import org.mozilla.fenix.GleanMetrics.ContextMenu import org.mozilla.fenix.GleanMetrics.ContextualHintTrackingProtection 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.LoginDialog 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.Onboarding import org.mozilla.fenix.GleanMetrics.Pings import org.mozilla.fenix.GleanMetrics.Pocket import org.mozilla.fenix.GleanMetrics.Preferences import org.mozilla.fenix.GleanMetrics.PrivateBrowsingMode import org.mozilla.fenix.GleanMetrics.PrivateBrowsingShortcut import org.mozilla.fenix.GleanMetrics.ProgressiveWebApp 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.SearchWidgetCfr import org.mozilla.fenix.GleanMetrics.SyncAccount import org.mozilla.fenix.GleanMetrics.SyncAuth import org.mozilla.fenix.GleanMetrics.Tab import org.mozilla.fenix.GleanMetrics.TabsTray import org.mozilla.fenix.GleanMetrics.Tip 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.GleanMetrics.VoiceSearch import org.mozilla.fenix.components.toolbar.ToolbarPosition 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.AppReceivedIntent -> EventWrapper( { Events.appReceivedIntent.record(it) }, { Events.appReceivedIntentKeys.valueOf(it) } ) is Event.AppOpenedAllSourceStartup -> EventWrapper( { Events.appOpenedAllStartup.record(it) }, { Events.appOpenedAllStartupKeys.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.SearchWithAds -> EventWrapper( { BrowserSearch.withAds[label].add(1) } ) is Event.SearchAdClicked -> EventWrapper( { BrowserSearch.adClicks[label].add(1) } ) is Event.SearchInContent -> EventWrapper( { BrowserSearch.inContent[label].add(1) } ) is Event.SearchShortcutSelected -> EventWrapper( { SearchShortcuts.selected.record(it) }, { SearchShortcuts.selectedKeys.valueOf(it) } ) is Event.LoginDialogPromptDisplayed -> EventWrapper( { LoginDialog.displayed.record(it) } ) is Event.LoginDialogPromptCancelled -> EventWrapper( { LoginDialog.cancelled.record(it) } ) is Event.LoginDialogPromptSave -> EventWrapper( { LoginDialog.saved.record(it) } ) is Event.LoginDialogPromptNeverSave -> EventWrapper( { LoginDialog.neverSave.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.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.SyncAuthUseEmail -> EventWrapper( { SyncAuth.useEmail.record(it) } ) is Event.SyncAuthUseEmailProblem -> EventWrapper( { SyncAuth.useEmailProblem.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.ReaderModeAvailable -> EventWrapper( { ReaderMode.available.record(it) } ) is Event.ReaderModeOpened -> EventWrapper( { ReaderMode.opened.record(it) } ) is Event.ReaderModeClosed -> EventWrapper( { ReaderMode.closed.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.DeleteLogin -> EventWrapper( { Logins.deleteSavedLogin.record(it) } ) is Event.EditLogin -> EventWrapper( { Logins.openLoginEditor.record(it) } ) is Event.EditLoginSave -> EventWrapper( { Logins.saveEditedLogin.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.TopSiteOpenDefault -> EventWrapper( { TopSites.openDefault.record(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) } ) is Event.AddonsOpenInSettings -> EventWrapper( { Addons.openAddonsInSettings.record(it) } ) is Event.AddonsOpenInToolbarMenu -> EventWrapper( { Addons.openAddonInToolbarMenu.record(it) }, { Addons.openAddonInToolbarMenuKeys.valueOf(it) } ) is Event.TipDisplayed -> EventWrapper( { Tip.displayed.record(it) }, { Tip.displayedKeys.valueOf(it) } ) is Event.TipPressed -> EventWrapper( { Tip.pressed.record(it) }, { Tip.pressedKeys.valueOf(it) } ) is Event.TipClosed -> EventWrapper( { Tip.closed.record(it) }, { Tip.closedKeys.valueOf(it) } ) is Event.VoiceSearchTapped -> EventWrapper( { VoiceSearch.tapped.record(it) } ) is Event.SearchWidgetCFRDisplayed -> EventWrapper( { SearchWidgetCfr.displayed.record(it) } ) is Event.SearchWidgetCFRCanceled -> EventWrapper( { SearchWidgetCfr.canceled.record(it) } ) is Event.SearchWidgetCFRNotNowPressed -> EventWrapper( { SearchWidgetCfr.notNowPressed.record(it) } ) is Event.SearchWidgetCFRAddWidgetPressed -> EventWrapper( { SearchWidgetCfr.addWidgetPressed.record(it) } ) is Event.TabCounterMenuItemTapped -> EventWrapper( { Events.tabCounterMenuAction.record(it) }, { Events.tabCounterMenuActionKeys.valueOf(it) } ) is Event.OnboardingWhatsNew -> EventWrapper( { Onboarding.whatsNew.record(it) } ) is Event.OnboardingPrivateBrowsing -> EventWrapper( { Onboarding.prefToggledPrivateBrowsing.record(it) } ) is Event.OnboardingPrivacyNotice -> EventWrapper( { Onboarding.privacyNotice.record(it) } ) is Event.OnboardingManualSignIn -> EventWrapper( { Onboarding.fxaManualSignin.record(it) } ) is Event.OnboardingAutoSignIn -> EventWrapper( { Onboarding.fxaAutoSignin.record(it) } ) is Event.OnboardingFinish -> EventWrapper( { Onboarding.finish.record(it) } ) is Event.OnboardingTrackingProtection -> EventWrapper( { Onboarding.prefToggledTrackingProt.record(it) }, { Onboarding.prefToggledTrackingProtKeys.valueOf(it) } ) is Event.OnboardingThemePicker -> EventWrapper( { Onboarding.prefToggledThemePicker.record(it) }, { Onboarding.prefToggledThemePickerKeys.valueOf(it) } ) is Event.OnboardingToolbarPosition -> EventWrapper( { Onboarding.prefToggledToolbarPosition.record(it) }, { Onboarding.prefToggledToolbarPositionKeys.valueOf(it) } ) is Event.ContextualHintETPDisplayed -> EventWrapper( { ContextualHintTrackingProtection.display.record(it) } ) is Event.ContextualHintETPDismissed -> EventWrapper( { ContextualHintTrackingProtection.dismiss.record(it) } ) is Event.ContextualHintETPInsideTap -> EventWrapper( { ContextualHintTrackingProtection.insideTap.record(it) } ) is Event.ContextualHintETPOutsideTap -> EventWrapper( { ContextualHintTrackingProtection.outsideTap.record(it) } ) is Event.TabsTrayOpened -> EventWrapper( { TabsTray.opened.record(it) } ) is Event.TabsTrayClosed -> EventWrapper( { TabsTray.closed.record(it) } ) is Event.OpenedExistingTab -> EventWrapper( { TabsTray.openedExistingTab.record(it) } ) is Event.ClosedExistingTab -> EventWrapper( { TabsTray.closedExistingTab.record(it) } ) is Event.TabsTrayPrivateModeTapped -> EventWrapper( { TabsTray.privateModeTapped.record(it) } ) is Event.TabsTrayNormalModeTapped -> EventWrapper( { TabsTray.normalModeTapped.record(it) } ) is Event.NewTabTapped -> EventWrapper( { TabsTray.newTabTapped.record(it) } ) is Event.NewPrivateTabTapped -> EventWrapper( { TabsTray.newPrivateTabTapped.record(it) } ) is Event.TabsTrayMenuOpened -> EventWrapper( { TabsTray.menuOpened.record(it) } ) is Event.TabsTraySaveToCollectionPressed -> EventWrapper( { TabsTray.saveToCollection.record(it) } ) is Event.TabsTrayShareAllTabsPressed -> EventWrapper( { TabsTray.shareAllTabs.record(it) } ) is Event.TabsTrayCloseAllTabsPressed -> EventWrapper( { TabsTray.closeAllTabs.record(it) } ) Event.AutoPlaySettingVisited -> EventWrapper( { Autoplay.visitedSetting.record(it) } ) is Event.AutoPlaySettingChanged -> EventWrapper( { Autoplay.settingChanged.record(it) }, { Autoplay.settingChangedKeys.valueOf(it) } ) is Event.ProgressiveWebAppOpenFromHomescreenTap -> EventWrapper( { ProgressiveWebApp.homescreenTap.record(it) } ) is Event.ProgressiveWebAppInstallAsShortcut -> EventWrapper( { ProgressiveWebApp.installTap.record(it) } ) is Event.ProgressiveWebAppForeground -> EventWrapper( { ProgressiveWebApp.foreground.record(it) }, { ProgressiveWebApp.foregroundKeys.valueOf(it) } ) is Event.ProgressiveWebAppBackground -> EventWrapper( { ProgressiveWebApp.background.record(it) }, { ProgressiveWebApp.backgroundKeys.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 is Event.AddonInstalled -> null is Event.SearchWidgetInstalled -> null is Event.ChangedToDefaultBrowser -> null } class GleanMetricsService(private val context: Context) : MetricsService { override val type = MetricServiceType.Data private val logger = Logger("GleanMetricsService") private var initialized = false private val activationPing = ActivationPing(context) private val installationPing = FirstSessionPing(context) override fun start() { logger.debug("Enabling Glean.") // Initialization of Glean already happened in FenixApplication. Glean.setUploadEnabled(true) if (initialized) return initialized = true // The code below doesn't need to execute immediately, so we'll add them to the visual // completeness task queue to be run later. context.components.performance.visualCompletenessQueue.queue.runIfReadyOrQueue { // 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. 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. setStartupMetrics() } } internal fun setStartupMetrics() { setPreferenceMetrics() 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) adjustAdGroup.set(context.settings().adjustAdGroup) adjustCreative.set(context.settings().adjustCreative) adjustNetwork.set(context.settings().adjustNetwork) searchWidgetInstalled.set(context.settings().searchWidgetInstalled) val openTabsCount = context.settings().openTabsCount hasOpenTabs.set(openTabsCount > 0) if (openTabsCount > 0) { tabsOpenCount.add(openTabsCount) } val topSitesSize = context.settings().topSitesSize hasTopSites.set(topSitesSize > 0) if (topSitesSize > 0) { topSitesCount.add(topSitesSize) } toolbarPosition.set( when (context.settings().toolbarPosition) { ToolbarPosition.BOTTOM -> Event.ToolbarPositionChanged.Position.BOTTOM.name ToolbarPosition.TOP -> 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.checkAndSend() } private fun setPreferenceMetrics() { // We purposefully make all of our preferences the string_list format to make data analysis // simpler. While it makes things like booleans a bit more complicated, it means all our // preferences can be analyzed with the same dashboard and compared. Preferences.apply { showSearchSuggestions.set(context.settings().shouldShowSearchSuggestions.toStringList()) remoteDebugging.set(context.settings().isRemoteDebuggingEnabled.toStringList()) telemetry.set(context.settings().isTelemetryEnabled.toStringList()) searchBrowsingHistory.set(context.settings().shouldShowHistorySuggestions.toStringList()) searchBookmarks.set(context.settings().shouldShowBookmarkSuggestions.toStringList()) showClipboardSuggestions.set(context.settings().shouldShowClipboardSuggestions.toStringList()) showSearchShortcuts.set(context.settings().shouldShowSearchShortcuts.toStringList()) openLinksInAPrivateTab.set(context.settings().openLinksInAPrivateTab.toStringList()) searchSuggestionsPrivate.set(context.settings().shouldShowSearchSuggestionsInPrivate.toStringList()) showVoiceSearch.set(context.settings().shouldShowVoiceSearch.toStringList()) openLinksInApp.set(context.settings().openLinksInExternalApp.toStringList()) val isLoggedIn = context.components.backgroundServices.accountManager.accountProfile() != null sync.set(isLoggedIn.toStringList()) val syncedItems = SyncEnginesStorage(context).getStatus().entries.filter { it.value }.map { it.key.nativeName } syncItems.set(syncedItems) val toolbarPositionSelection = if (context.settings().shouldUseFixedTopToolbar) { "fixed_top" } else if (context.settings().shouldUseBottomToolbar) { "bottom" } else { "top" } toolbarPosition.set(listOf(toolbarPositionSelection)) val etpSelection = if (!context.settings().shouldUseTrackingProtection) { "" } else if (context.settings().useStandardTrackingProtection) { "standard" } else if (context.settings().useStrictTrackingProtection) { "strict" } else if (context.settings().useCustomTrackingProtection) { "custom" } else { "" } trackingProtection.set(listOf(etpSelection)) val accessibilitySelection = mutableListOf() if (context.settings().switchServiceIsEnabled) { accessibilitySelection.add("switch") } if (context.settings().touchExplorationIsEnabled) { accessibilitySelection.add("touch exploration") } accessibilityServices.set(accessibilitySelection.toList()) val themeSelection = if (context.settings().shouldUseLightTheme) { "light" } else if (context.settings().shouldUseDarkTheme) { "dark" } else if (context.settings().shouldFollowDeviceTheme) { "system" } else if (context.settings().shouldUseAutoBatteryTheme) { "battery" } else { "" } theme.set(listOf(themeSelection)) } } override fun stop() { Glean.setUploadEnabled(false) } override fun track(event: Event) { event.wrapper?.track(event) } override fun shouldTrack(event: Event): Boolean { return event.wrapper != null } } // Helper function for making our booleans fit into the string list formatting fun Boolean.toStringList(): List { return listOf(this.toString()) }