chromium/chrome/browser/ui/android/omnibox/java/src/org/chromium/chrome/browser/omnibox/OmniboxMetrics.java

// Copyright 2020 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

package org.chromium.chrome.browser.omnibox;

import androidx.annotation.IntDef;
import androidx.annotation.NonNull;
import androidx.annotation.VisibleForTesting;

import org.chromium.base.metrics.RecordHistogram;
import org.chromium.base.metrics.TimingMetric;
import org.chromium.chrome.browser.omnibox.suggestions.mostvisited.SuggestTileType;
import org.chromium.components.metrics.OmniboxEventProtos.OmniboxEventProto.PageClassification;
import org.chromium.components.omnibox.AutocompleteMatch;
import org.chromium.components.omnibox.suggestions.OmniboxSuggestionUiType;

import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.Optional;

/** This class collects a variety of different Omnibox related metrics. */
public class OmniboxMetrics {
    /**
     * Maximum number of suggest tile types we want to record. Anything beyond this will be reported
     * in the overflow bucket.
     */
    private static final int MAX_SUGGEST_TILE_TYPE_POSITION = 15;

    public static final int MAX_AUTOCOMPLETE_POSITION = 30;

    /**
     * Duration between the request for suggestions and the time the first (synchronous) reply is
     * converted to the UI model.
     */
    @VisibleForTesting
    public static final String HISTOGRAM_SUGGESTIONS_REQUEST_TO_UI_MODEL_FIRST =
            "Android.Omnibox.SuggestionList.RequestToUiModel.First";

    /**
     * Duration between the request for suggestions and the time the last (asynchronous) reply is
     * converted to the UI model.
     */
    @VisibleForTesting
    public static final String HISTOGRAM_SUGGESTIONS_REQUEST_TO_UI_MODEL_LAST =
            "Android.Omnibox.SuggestionList.RequestToUiModel.Last";

    /** Android.Omnibox.OmniboxAction.* histograms */
    @VisibleForTesting
    public static final String HISTOGRAM_OMNIBOX_ACTION_USED = "Android.Omnibox.OmniboxAction.Used";

    @VisibleForTesting
    public static final String HISTOGRAM_OMNIBOX_ACTION_VALID =
            "Android.Omnibox.OmniboxAction.Valid";

    /**
     * The amount of time it takes to process a touch down event. A touch down event can send a
     * signal to native to start a prefetch for the suggestion.
     */
    public static final String HISTOGRAM_SEARCH_PREFETCH_TOUCH_DOWN_PROCESS_TIME =
            "Android.Omnibox.SearchPrefetch.TouchDownProcessTime.NavigationPrefetch";

    /** The number of prefetches started in an omnibox session via the touch down trigger. */
    public static final String HISTOGRAM_SEARCH_PREFETCH_NUM_PREFETCHES_STARTED_IN_OMNIBOX_SESSION =
            "Android.Omnibox.SearchPrefetch.NumPrefetchesStartedInOmniboxSession.NavigationPrefetch";

    /** The result of prefetches started by touch down events within an omnibox session */
    public static final String HISTOGRAM_SEARCH_PREFETCH_TOUCH_DOWN_PREFETCH_RESULT =
            "Android.Omnibox.SearchPrefetch.TouchDownPrefetchResult.NavigationPrefetch";

    @IntDef({
        RefineActionUsage.NOT_USED,
        RefineActionUsage.SEARCH_WITH_ZERO_PREFIX,
        RefineActionUsage.SEARCH_WITH_PREFIX,
        RefineActionUsage.SEARCH_WITH_BOTH,
        RefineActionUsage.COUNT
    })
    @Retention(RetentionPolicy.SOURCE)
    public @interface RefineActionUsage {
        int NOT_USED = 0; // User did not interact with Refine button.
        int SEARCH_WITH_ZERO_PREFIX = 1; // User interacted with Refine button in zero-prefix mode.
        int SEARCH_WITH_PREFIX = 2; // User interacted with Refine button in non-zero-prefix mode.
        int SEARCH_WITH_BOTH = 3; // User interacted with Refine button in both contexts.
        int COUNT = 4;
    }

    @IntDef({
        ActionInSuggestIntentResult.SUCCESS,
        ActionInSuggestIntentResult.BAD_URI_SYNTAX,
        ActionInSuggestIntentResult.ACTIVITY_NOT_FOUND,
        ActionInSuggestIntentResult.COUNT
    })
    @Retention(RetentionPolicy.SOURCE)
    public @interface ActionInSuggestIntentResult {
        /// Intent started successfully.
        int SUCCESS = 0;
        /// Unable to deserialize intent: invalid syntax. Never recorded: http://b/279756377.
        int BAD_URI_SYNTAX = 1;
        /// Unable to start intent: no activity.
        int ACTIVITY_NOT_FOUND = 2;
        int COUNT = 3;
    }

    @IntDef({
        PrefetchResult.HIT,
        PrefetchResult.MISS,
        PrefetchResult.NO_PREFETCH,
        PrefetchResult.COUNT
    })
    @Retention(RetentionPolicy.SOURCE)
    public @interface PrefetchResult {
        // The last prefetch started matches the suggestion navigated to.
        int HIT = 0;
        // The last prefetch started does NOT match the suggesiton navigated to.
        int MISS = 1;
        // No prefetches were stated in the omnibox session.
        int NO_PREFETCH = 2;
        int COUNT = 3;
    }

    /**
     * Record how long the Suggestion List needed to layout its content and children in thread time.
     */
    public static TimingMetric recordSuggestionListLayoutTime() {
        return TimingMetric.shortThreadTime("Android.Omnibox.SuggestionList.LayoutTime2");
    }

    /**
     * Record how long the Suggestion List needed to layout its content and children in wall time.
     */
    public static TimingMetric recordSuggestionListLayoutWallTime() {
        return TimingMetric.shortUptime("Android.Omnibox.SuggestionList.LayoutTime3");
    }

    /**
     * Record how long the Suggestion List needed to measure its content and children in thread
     * time.
     */
    public static TimingMetric recordSuggestionListMeasureTime() {
        return TimingMetric.shortThreadTime("Android.Omnibox.SuggestionList.MeasureTime2");
    }

    /**
     * Record how long the Suggestion List needed to measure its content and children in wall time.
     */
    public static TimingMetric recordSuggestionListMeasureWallTime() {
        return TimingMetric.shortThreadTime("Android.Omnibox.SuggestionList.MeasureTime3");
    }

    /**
     * Record the amount of time needed to create a new suggestion view. The type of view is
     * intentionally ignored for this call.
     */
    public static TimingMetric recordSuggestionViewCreateTime() {
        return TimingMetric.shortThreadTime("Android.Omnibox.SuggestionView.CreateTime2");
    }

    /** Record the amount of wall time needed to create a new suggestion view. */
    public static TimingMetric recordSuggestionViewCreateWallTime() {
        return TimingMetric.shortThreadTime("Android.Omnibox.SuggestionView.CreateTime3");
    }

    /**
     * Record whether suggestion view was successfully reused.
     *
     * @param viewsCreated Number of views created during the input session. This should not be
     *     higher than the sum of all limits in HistogramRecordingRecycledViewPool.
     * @param viewsReused Ratio of views re-used to total views bound. Effectively captures the
     *     efficiency of view recycling.
     */
    public static void recordSuggestionViewReuseStats(int viewsCreated, int viewsReused) {
        RecordHistogram.recordCount100Histogram(
                "Android.Omnibox.SuggestionView.SessionViewsCreated", viewsCreated);
        RecordHistogram.recordCount100Histogram(
                "Android.Omnibox.SuggestionView.SessionViewsReused", viewsReused);
    }

    /**
     * Record the type of the suggestion view that had to be constructed. Recorded view type could
     * not be retrieved from the Recycled View Pool and had to be re-created. Relevant for Omnibox
     * recycler view improvements.
     *
     * @param type The type of view that needed to be recreated.
     */
    public static void recordSuggestionsViewCreatedType(@OmniboxSuggestionUiType int type) {
        RecordHistogram.recordEnumeratedHistogram(
                "Android.Omnibox.SuggestionView.CreatedType", type, OmniboxSuggestionUiType.COUNT);
    }

    /**
     * Record the type of the suggestion view that was re-used. Recorded view type was retrieved
     * from the Recycled View Pool. Relevant for Omnibox recycler view improvements.
     *
     * @param type The type of view that was reused from pool.
     */
    public static void recordSuggestionsViewReusedType(@OmniboxSuggestionUiType int type) {
        RecordHistogram.recordEnumeratedHistogram(
                "Android.Omnibox.SuggestionView.ReusedType", type, OmniboxSuggestionUiType.COUNT);
    }

    /**
     * Record whether the interaction with the Omnibox resulted with a navigation (true) or user
     * leaving the omnibox and suggestions list.
     *
     * @param focusResultedInNavigation Whether the user completed interaction with navigation.
     */
    public static void recordOmniboxFocusResultedInNavigation(boolean focusResultedInNavigation) {
        RecordHistogram.recordBooleanHistogram(
                "Omnibox.FocusResultedInNavigation", focusResultedInNavigation);
    }

    /**
     * Record the length of time between when omnibox gets focused and when a omnibox match is open.
     */
    public static void recordFocusToOpenTime(long focusToOpenTimeInMillis) {
        RecordHistogram.recordMediumTimesHistogram(
                "Omnibox.FocusToOpenTimeAnyPopupState3", focusToOpenTimeInMillis);
    }

    /**
     * Record whether the used suggestion originates from Cache or Autocomplete subsystem.
     *
     * @param isFromCache Whether the suggestion selected by the User comes from suggestion cache.
     */
    public static void recordUsedSuggestionFromCache(boolean isFromCache) {
        RecordHistogram.recordBooleanHistogram(
                "Android.Omnibox.UsedSuggestionFromCache", isFromCache);
    }

    /**
     * Record the Refine action button usage. Unlike the MobileOmniobxRefineSuggestion UserAction,
     * this is recorded only once at the end of an Omnibox interaction, and includes the cases where
     * the user has not interacted with the Refine button at all.
     *
     * @param refineActionUsage Whether - and how Refine action button was used.
     */
    public static void recordRefineActionUsage(@RefineActionUsage int refineActionUsage) {
        RecordHistogram.recordEnumeratedHistogram(
                "Android.Omnibox.RefineActionUsage", refineActionUsage, RefineActionUsage.COUNT);
    }

    /**
     * Record page class specific histogram reflecting whether the user scrolled the suggestions
     * list.
     *
     * @param pageClass Page classification.
     * @param wasScrolled Whether the suggestions list was scrolled.
     */
    public static void recordSuggestionsListScrolled(int pageClass, boolean wasScrolled) {
        RecordHistogram.recordBooleanHistogram(
                histogramName("Android.Omnibox.SuggestionsListScrolled", pageClass), wasScrolled);
    }

    /**
     * Record the kind of (MostVisitedURL/OrganicRepeatableSearch) Tile type the User opened.
     *
     * @param position The position of a tile in the carousel.
     * @param isSearchTile Whether tile being opened is a Search tile.
     */
    public static void recordSuggestTileTypeUsed(int position, boolean isSearchTile) {
        @SuggestTileType int tileType = isSearchTile ? SuggestTileType.SEARCH : SuggestTileType.URL;
        RecordHistogram.recordExactLinearHistogram(
                "Omnibox.SuggestTiles.SelectedTileIndex", position, MAX_SUGGEST_TILE_TYPE_POSITION);
        RecordHistogram.recordEnumeratedHistogram(
                "Omnibox.SuggestTiles.SelectedTileType", tileType, SuggestTileType.COUNT);
    }

    /**
     * Records relevant histogram(s) when a Journeys action is clicked in the omnibox. Not emitted
     * if the given position is <0.
     */
    public static void recordResumeJourneyClick(int position) {
        if (position < 0) return;
        RecordHistogram.recordExactLinearHistogram(
                "Omnibox.SuggestionUsed.ResumeJourney", position, MAX_AUTOCOMPLETE_POSITION);
    }

    /**
     * Records relevant histogram(s) when a Journeys action is shown in the omnibox. Not emitted if
     * the given position is <0.
     */
    public static void recordResumeJourneyShown(int position) {
        if (position < 0) return;
        RecordHistogram.recordEnumeratedHistogram(
                "Omnibox.ResumeJourneyShown", position, MAX_AUTOCOMPLETE_POSITION);
    }

    /**
     * Records the time elapsed between the two events:
     *
     * <ul>
     *   <li>the suggestions were requested (as a result of User input), and
     *   <li>the suggestions response was transformed to a UI model.
     * </ul>
     *
     * @param isFirst specifies whether this is the first (synchronous), or the last (final)
     *     asynchronous, suggestions response received from the AutocompleteController
     * @param elapsedTimeMs specifies how much time has elapsed between the two events
     */
    public static void recordSuggestionRequestToModelTime(boolean isFirst, long elapsedTimeMs) {
        RecordHistogram.recordCustomTimesHistogram(
                isFirst
                        ? HISTOGRAM_SUGGESTIONS_REQUEST_TO_UI_MODEL_FIRST
                        : HISTOGRAM_SUGGESTIONS_REQUEST_TO_UI_MODEL_LAST,
                elapsedTimeMs,
                1,
                1000,
                50);
    }

    /**
     * Record the outcome of ActionInSuggest chip interaction.
     *
     * @param intentResult the {@link #ActionInSuggestIntentResult} to record
     */
    public static final void recordActionInSuggestIntentResult(
            @ActionInSuggestIntentResult int intentResult) {
        RecordHistogram.recordEnumeratedHistogram(
                "Android.Omnibox.ActionInSuggest.IntentResult",
                intentResult,
                ActionInSuggestIntentResult.COUNT);
    }

    /**
     * Record whether an OmniboxAction was still valid when the user finished the interaction with
     * the Omnibox.
     *
     * <p>Recorded once for every action currently offered to the user at the time when the user
     * completed the interaction with the Omnibox.
     */
    public static void recordOmniboxActionIsValid(boolean isValid) {
        RecordHistogram.recordBooleanHistogram(HISTOGRAM_OMNIBOX_ACTION_VALID, isValid);
    }

    /**
     * Record whether any OmniboxAction was used by the User to complete interaction with the
     * Omnibox.
     *
     * <p>Recorded once for *every interaction with the Omnibox* where OmniboxActions were shown to
     * the user at the final stage of interaction.
     */
    public static void recordOmniboxActionIsUsed(boolean wasUsed) {
        RecordHistogram.recordBooleanHistogram(HISTOGRAM_OMNIBOX_ACTION_USED, wasUsed);
    }

    /**
     * Record the amount of time it takes to process a touch down event. This can include sending
     * event to native and then starting a prefetch for the suggestion.
     */
    public static TimingMetric recordTouchDownProcessTime() {
        return TimingMetric.shortThreadTime(HISTOGRAM_SEARCH_PREFETCH_TOUCH_DOWN_PROCESS_TIME);
    }

    /**
     * Records the number of prefetches started by touch down events in an omnibox session.
     *
     * @param numPrefetchesStarted the number of prefetches started wihin the omnibox session.
     */
    public static void recordNumPrefetchesStartedInOmniboxSession(int numPrefetchesStarted) {
        RecordHistogram.recordCount100Histogram(
                HISTOGRAM_SEARCH_PREFETCH_NUM_PREFETCHES_STARTED_IN_OMNIBOX_SESSION,
                numPrefetchesStarted);
    }

    /**
     * Records the result of prefetches started by touch down events within an omnibox session.
     *
     * @param navSuggestion the suggestion that was navigated to.
     * @param prefetchSuggestion the most recent suggestion that a prefetch was started for. This
     *     value is null if no prefetches have been started in the current omnibox session.
     */
    public static void recordTouchDownPrefetchResult(
            @NonNull AutocompleteMatch navSuggestion,
            @NonNull Optional<AutocompleteMatch> prefetchSuggestion) {
        @PrefetchResult
        int result =
                prefetchSuggestion
                        .map(
                                match ->
                                        navSuggestion.getNativeObjectRef() != 0
                                                        && navSuggestion.getNativeObjectRef()
                                                                == match.getNativeObjectRef()
                                                ? PrefetchResult.HIT
                                                : PrefetchResult.MISS)
                        .orElse(PrefetchResult.NO_PREFETCH);

        RecordHistogram.recordEnumeratedHistogram(
                HISTOGRAM_SEARCH_PREFETCH_TOUCH_DOWN_PREFETCH_RESULT, result, PrefetchResult.COUNT);
    }

    /**
     * Translate the pageClass to a histogram suffix.
     *
     * @param histogram Histogram prefix.
     * @param pageClass Page classification to translate.
     * @return Metric name.
     */
    private static String histogramName(@NonNull String prefix, int pageClass) {
        String suffix = "Other";

        switch (pageClass) {
            case PageClassification.INSTANT_NTP_WITH_OMNIBOX_AS_STARTING_FOCUS_VALUE:
            case PageClassification.NTP_REALBOX_VALUE:
            case PageClassification.NTP_VALUE:
            case PageClassification.NTP_ZPS_PREFETCH_VALUE:
            case PageClassification.SEARCH_BUTTON_AS_STARTING_FOCUS_VALUE:
            case PageClassification.START_SURFACE_HOMEPAGE_VALUE:
            case PageClassification.START_SURFACE_NEW_TAB_VALUE:
                suffix = "NTP";
                break;

            case PageClassification.LENS_SIDE_PANEL_SEARCHBOX_VALUE:
            case PageClassification.SEARCH_RESULT_PAGE_DOING_SEARCH_TERM_REPLACEMENT_VALUE:
            case PageClassification.SEARCH_RESULT_PAGE_NO_SEARCH_TERM_REPLACEMENT_VALUE:
            case PageClassification.SEARCH_RESULT_PAGE_ON_CCT_VALUE:
            case PageClassification.SEARCH_SIDE_PANEL_SEARCHBOX_VALUE:
            case PageClassification.SRP_ZPS_PREFETCH_VALUE:
                suffix = "SRP";
                break;

            case PageClassification.ANDROID_SEARCH_WIDGET_VALUE:
            case PageClassification.ANDROID_SHORTCUTS_WIDGET_VALUE:
                suffix = "Widget";
                break;

            case PageClassification.BLANK_VALUE:
            case PageClassification.CONTEXTUAL_SEARCHBOX_VALUE:
            case PageClassification.HOME_PAGE_VALUE:
            case PageClassification.JOURNEYS_VALUE:
            case PageClassification.OTHER_ON_CCT_VALUE:
            case PageClassification.OTHER_VALUE:
            case PageClassification.OTHER_ZPS_PREFETCH_VALUE:
                // use default value for websites.
                break;

            case PageClassification.OBSOLETE_INSTANT_NTP_VALUE:
            case PageClassification.OBSOLETE_INSTANT_NTP_WITH_FAKEBOX_AS_STARTING_FOCUS_VALUE:
                assert false
                        : "Obsolete page classification. Please use the OMNIBOX variant instead.";
                break;

            default:
                // May trigger if nev PageClassifications were added to
                // third_party/metrics_proto/omnibox_event.proto file,
                // but have not been reflected here. If that's the case, file a bug for the
                // author of the new PageClassification.
                // Last supported value: OTHER_ON_CCT.
                assert false
                        : "b/40221519: Invalid page classification: "
                                + pageClass
                                + ". Please re-open bug, and attach captured stack trace.";
                break;
        }

        return prefix + "." + suffix;
    }
}