1 // Copyright 2015 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
4 
5 package org.chromium.chrome.browser.compositor.bottombar.contextualsearch;
6 
7 import org.chromium.base.TimeUtils;
8 import org.chromium.chrome.browser.compositor.bottombar.OverlayPanel.PanelState;
9 import org.chromium.chrome.browser.compositor.bottombar.OverlayPanel.StateChangeReason;
10 import org.chromium.chrome.browser.contextualsearch.ContextualSearchHeuristics;
11 import org.chromium.chrome.browser.contextualsearch.ContextualSearchIPH;
12 import org.chromium.chrome.browser.contextualsearch.ContextualSearchInteractionRecorder;
13 import org.chromium.chrome.browser.contextualsearch.ContextualSearchUma;
14 import org.chromium.chrome.browser.contextualsearch.EngagementSuppression;
15 import org.chromium.chrome.browser.contextualsearch.QuickActionCategory;
16 import org.chromium.chrome.browser.contextualsearch.ResolvedSearchTerm;
17 import org.chromium.chrome.browser.profiles.Profile;
18 
19 /**
20  * This class is responsible for all the logging triggered by activity of the
21  * {@link ContextualSearchPanel}. Typically this consists of tracking user activity
22  * logging that to UMA when the interaction ends as the panel is dismissed.
23  */
24 public class ContextualSearchPanelMetrics {
25     // Flags for logging.
26     private boolean mDidSearchInvolvePromo;
27     private boolean mWasSearchContentViewSeen;
28     private boolean mIsPromoActive;
29     private boolean mHasExpanded;
30     private boolean mHasMaximized;
31     private boolean mHasExitedPeeking;
32     private boolean mHasExitedExpanded;
33     private boolean mHasExitedMaximized;
34     private boolean mIsSerpNavigation;
35     private boolean mWasActivatedByTap;
36     private boolean mWasPanelOpenedBeyondPeek;
37     private boolean mWasContextualCardsDataShown;
38     @ResolvedSearchTerm.CardTag
39     private int mCardTag;
40     private boolean mWasQuickActionShown;
41     private int mQuickActionCategory;
42     private boolean mWasQuickActionClicked;
43     private int mSelectionLength;
44     // Whether any Tap suppression heuristic was satisfied when the panel was shown.
45     private boolean mWasAnyHeuristicSatisfiedOnPanelShow;
46     // Time when the panel was triggered (not reset by a chained search).
47     // Panel transitions are animated so mPanelTriggerTimeNs will be less than mFirstPeekTimeNs.
48     private long mPanelTriggerTimeFromTapNs;
49     // Time when the panel peeks into view (not reset by a chained search).
50     // Used to log total time the panel is showing (not closed).
51     private long mFirstPeekTimeNs;
52     // Time when a search request was started. Reset by chained searches.
53     // Used to log the time it takes for a Search Result to become available.
54     private long mSearchRequestStartTimeNs;
55     // Time when the panel was opened beyond peeked. Reset when the panel is closed.
56     // Used to log how long the panel was open.
57     private long mPanelOpenedBeyondPeekTimeNs;
58     // Duration that the panel was opened beyond peeked. Reset when the panel is closed.
59     // Used to log how long the panel was open.
60     private long mPanelOpenedBeyondPeekDurationMs;
61     // The current set of heuristics that should be logged with results seen when the panel closes.
62     private ContextualSearchHeuristics mResultsSeenExperiments;
63     // The interaction recorder to use to record results seen when the panel closes.
64     private ContextualSearchInteractionRecorder mInteractionRecorder;
65     // Whether interaction Outcomes are valid, because we showed the panel.
66     private boolean mAreOutcomesValid;
67 
68     /**
69      * Log information when the panel's state has changed.
70      * @param fromState The state the panel is transitioning from.
71      * @param toState The state that the panel is transitioning to.
72      * @param reason The reason for the state change.
73      * @param profile The current {@link Profile}.
74      */
onPanelStateChanged(@anelState int fromState, @PanelState int toState, @StateChangeReason int reason, Profile profile)75     public void onPanelStateChanged(@PanelState int fromState, @PanelState int toState,
76             @StateChangeReason int reason, Profile profile) {
77         // Note: the logging within this function includes the promo, unless specifically
78         // excluded.
79         boolean isStartingSearch = isStartingNewContextualSearch(toState, reason);
80         boolean isEndingSearch = isEndingContextualSearch(fromState, toState, isStartingSearch);
81         boolean isChained = isStartingSearch && isOngoingContextualSearch(fromState);
82         boolean isSameState = fromState == toState;
83         boolean isFirstExitFromPeeking = fromState == PanelState.PEEKED && !mHasExitedPeeking
84                 && (!isSameState || isStartingSearch);
85         boolean isFirstExitFromExpanded = fromState == PanelState.EXPANDED && !mHasExitedExpanded
86                 && !isSameState;
87         boolean isFirstExitFromMaximized = fromState == PanelState.MAXIMIZED && !mHasExitedMaximized
88                 && !isSameState;
89         boolean isContentVisible =
90                 toState == PanelState.MAXIMIZED || toState == PanelState.EXPANDED;
91         boolean isExitingPanelOpenedBeyondPeeked = mWasPanelOpenedBeyondPeek && !isContentVisible;
92 
93         if (toState == PanelState.CLOSED && mPanelTriggerTimeFromTapNs != 0
94                 && reason == StateChangeReason.BASE_PAGE_SCROLL) {
95             long durationMs = (System.nanoTime() - mPanelTriggerTimeFromTapNs)
96                     / TimeUtils.NANOSECONDS_PER_MILLISECOND;
97             ContextualSearchUma.logDurationBetweenTriggerAndScroll(
98                     durationMs, mWasSearchContentViewSeen);
99         }
100 
101         if (isExitingPanelOpenedBeyondPeeked) {
102             assert mPanelOpenedBeyondPeekTimeNs != 0;
103             mPanelOpenedBeyondPeekDurationMs = (System.nanoTime() - mPanelOpenedBeyondPeekTimeNs)
104                     / TimeUtils.NANOSECONDS_PER_MILLISECOND;
105             ContextualSearchUma.logPanelOpenDuration(mPanelOpenedBeyondPeekDurationMs);
106             mPanelOpenedBeyondPeekTimeNs = 0;
107             mWasPanelOpenedBeyondPeek = false;
108         }
109 
110         if (isEndingSearch) {
111             long panelViewDurationMs =
112                     (System.nanoTime() - mFirstPeekTimeNs) / TimeUtils.NANOSECONDS_PER_MILLISECOND;
113             ContextualSearchUma.logPanelViewDurationAction(panelViewDurationMs);
114             if (!mDidSearchInvolvePromo) {
115                 // Measure duration only when the promo is not involved.
116                 ContextualSearchUma.logDuration(
117                         mWasSearchContentViewSeen, isChained, panelViewDurationMs);
118             }
119             if (mIsPromoActive) {
120                 // The user is exiting still in the promo, without choosing an option.
121                 ContextualSearchUma.logPromoSeen(mWasSearchContentViewSeen, mWasActivatedByTap);
122             } else {
123                 ContextualSearchUma.logResultsSeen(mWasSearchContentViewSeen, mWasActivatedByTap);
124             }
125 
126             if (mWasContextualCardsDataShown) {
127                 ContextualSearchUma.logContextualCardsResultsSeen(mWasSearchContentViewSeen);
128                 EngagementSuppression.registerContextualCardsImpression(mWasSearchContentViewSeen);
129             }
130             ContextualSearchUma.logCardTagSeen(mWasSearchContentViewSeen, mCardTag);
131             if (mWasQuickActionShown) {
132                 ContextualSearchUma.logQuickActionResultsSeen(mWasSearchContentViewSeen,
133                         mQuickActionCategory);
134                 ContextualSearchUma.logQuickActionClicked(mWasQuickActionClicked,
135                         mQuickActionCategory);
136                 EngagementSuppression.registerQuickActionImpression(
137                         mWasSearchContentViewSeen, mWasQuickActionClicked);
138             }
139 
140             if (mResultsSeenExperiments != null) {
141                 mResultsSeenExperiments.logResultsSeen(
142                         mWasSearchContentViewSeen, mWasActivatedByTap);
143                 mResultsSeenExperiments.logPanelViewedDurations(
144                         panelViewDurationMs, mPanelOpenedBeyondPeekDurationMs);
145                 if (!isChained) mResultsSeenExperiments = null;
146             }
147             mPanelOpenedBeyondPeekDurationMs = 0;
148 
149             if (mWasActivatedByTap) {
150                 boolean wasAnySuppressionHeuristicSatisfied = mWasAnyHeuristicSatisfiedOnPanelShow;
151                 ContextualSearchUma.logAnyTapSuppressionHeuristicSatisfied(
152                         mWasSearchContentViewSeen, wasAnySuppressionHeuristicSatisfied);
153                 ContextualSearchUma.logSelectionLengthResultsSeen(
154                         mWasSearchContentViewSeen, mSelectionLength);
155                 ContextualSearchUma.logTapResultsSeen(mWasSearchContentViewSeen);
156             }
157             ContextualSearchUma.logAllResultsSeen(mWasSearchContentViewSeen);
158 
159             // Notifications to Feature Engagement.
160             ContextualSearchIPH.doSearchFinishedNotifications(profile, mWasSearchContentViewSeen,
161                     mWasActivatedByTap, mWasContextualCardsDataShown);
162 
163             writeInteractionOutcomesAndReset();
164         }
165 
166         if (isStartingSearch) {
167             mFirstPeekTimeNs = System.nanoTime();
168             mWasActivatedByTap = reason == StateChangeReason.TEXT_SELECT_TAP;
169             if (mWasActivatedByTap && mResultsSeenExperiments != null) {
170                 mWasAnyHeuristicSatisfiedOnPanelShow =
171                         mResultsSeenExperiments.isAnyConditionSatisfiedForAggregrateLogging();
172             } else {
173                 mWasAnyHeuristicSatisfiedOnPanelShow = false;
174             }
175             mAreOutcomesValid = true;
176         }
177 
178         // Log state changes. We only log the first transition to a state within a contextual
179         // search. Note that when a user clicks on a link on the search content view, this will
180         // trigger a transition to MAXIMIZED (SERP_NAVIGATION) followed by a transition to
181         // CLOSED (TAB_PROMOTION). For the purpose of logging, the reason for the second transition
182         // is reinterpreted to SERP_NAVIGATION, in order to distinguish it from a tab promotion
183         // caused when tapping on the Search Bar when the Panel is maximized.
184         @StateChangeReason
185         int reasonForLogging = mIsSerpNavigation ? StateChangeReason.SERP_NAVIGATION : reason;
186         if (isStartingSearch || isEndingSearch
187                 || (!mHasExpanded && toState == PanelState.EXPANDED)
188                 || (!mHasMaximized && toState == PanelState.MAXIMIZED)) {
189             ContextualSearchUma.logFirstStateEntry(fromState, toState, reasonForLogging);
190         }
191         // Note: CLOSED / UNDEFINED state exits are detected when a search that is not chained is
192         // starting.
193         if ((isStartingSearch && !isChained) || isFirstExitFromPeeking || isFirstExitFromExpanded
194                 || isFirstExitFromMaximized) {
195             ContextualSearchUma.logFirstStateExit(fromState, toState, reasonForLogging);
196         }
197         // Log individual user actions so they can be sequenced.
198         ContextualSearchUma.logPanelStateUserAction(toState, reasonForLogging);
199 
200         // We can now modify the state.
201         if (isFirstExitFromPeeking) {
202             mHasExitedPeeking = true;
203         } else if (isFirstExitFromExpanded) {
204             mHasExitedExpanded = true;
205         } else if (isFirstExitFromMaximized) {
206             mHasExitedMaximized = true;
207         }
208 
209         if (toState == PanelState.EXPANDED) {
210             mHasExpanded = true;
211         } else if (toState == PanelState.MAXIMIZED) {
212             mHasMaximized = true;
213         }
214         if (reason == StateChangeReason.SERP_NAVIGATION) {
215             mIsSerpNavigation = true;
216         }
217 
218         if (isEndingSearch) {
219             mDidSearchInvolvePromo = false;
220             mWasSearchContentViewSeen = false;
221             mHasExpanded = false;
222             mHasMaximized = false;
223             mHasExitedPeeking = false;
224             mHasExitedExpanded = false;
225             mHasExitedMaximized = false;
226             mIsSerpNavigation = false;
227             mWasContextualCardsDataShown = false;
228             mWasQuickActionShown = false;
229             mQuickActionCategory = QuickActionCategory.NONE;
230             mCardTag = ResolvedSearchTerm.CardTag.CT_NONE;
231             mWasQuickActionClicked = false;
232             mWasAnyHeuristicSatisfiedOnPanelShow = false;
233             mPanelTriggerTimeFromTapNs = 0;
234         }
235     }
236 
237     /**
238      * Sets that the contextual search involved the promo.
239      */
setDidSearchInvolvePromo()240     public void setDidSearchInvolvePromo() {
241         mDidSearchInvolvePromo = true;
242     }
243 
244     /**
245      * Sets that the Search Content View was seen.
246      */
setWasSearchContentViewSeen()247     public void setWasSearchContentViewSeen() {
248         mWasSearchContentViewSeen = true;
249         mWasPanelOpenedBeyondPeek = true;
250         mPanelOpenedBeyondPeekTimeNs = System.nanoTime();
251         mPanelOpenedBeyondPeekDurationMs = 0;
252     }
253 
254     /**
255      * Sets whether the promo is active.
256      */
setIsPromoActive(boolean shown)257     public void setIsPromoActive(boolean shown) {
258         mIsPromoActive = shown;
259     }
260 
261     /**
262      * @param wasContextualCardsDataShown Whether Contextual Cards data was shown in the Contextual
263      *                                    Search Bar.
264      */
setWasContextualCardsDataShown( boolean wasContextualCardsDataShown, @ResolvedSearchTerm.CardTag int cardTag)265     public void setWasContextualCardsDataShown(
266             boolean wasContextualCardsDataShown, @ResolvedSearchTerm.CardTag int cardTag) {
267         mWasContextualCardsDataShown = wasContextualCardsDataShown;
268         mCardTag = cardTag;
269     }
270 
271     /**
272      * @param wasQuickActionShown Whether a quick action was shown in the Contextual Search Bar.
273      * @param quickActionCategory The {@link QuickActionCategory} for the quick action.
274      */
setWasQuickActionShown(boolean wasQuickActionShown, int quickActionCategory)275     public void setWasQuickActionShown(boolean wasQuickActionShown, int quickActionCategory) {
276         mWasQuickActionShown = wasQuickActionShown;
277         if (mWasQuickActionShown) mQuickActionCategory = quickActionCategory;
278     }
279 
280     /**
281      * Sets |mWasQuickActionClicked| to true.
282      */
setWasQuickActionClicked()283     public void setWasQuickActionClicked() {
284         mWasQuickActionClicked = true;
285     }
286 
287     /**
288      * Should be called when the panel first starts showing due to a tap.
289      */
onPanelTriggeredFromTap()290     public void onPanelTriggeredFromTap() {
291         mPanelTriggerTimeFromTapNs = System.nanoTime();
292     }
293 
294     /**
295      * @param selection The text that is selected when a selection is established.
296      */
onSelectionEstablished(String selection)297     public void onSelectionEstablished(String selection) {
298         mSelectionLength = selection.length();
299     }
300 
301     /**
302      * Called to record the time when a search request started, for resolve and prefetch timing.
303      */
onSearchRequestStarted()304     public void onSearchRequestStarted() {
305         mSearchRequestStartTimeNs = System.nanoTime();
306     }
307 
308     /**
309      * Called when a Search Term has been resolved.
310      */
onSearchTermResolved()311     public void onSearchTermResolved() {
312         long durationMs = (System.nanoTime() - mSearchRequestStartTimeNs)
313                 / TimeUtils.NANOSECONDS_PER_MILLISECOND;
314         ContextualSearchUma.logSearchTermResolutionDuration(durationMs);
315     }
316 
317     /**
318      * Called after the panel has navigated to prefetched Search Results.
319      * This is the point where the search result starts to render in the panel.
320      */
onPanelNavigatedToPrefetchedSearch(boolean didResolve)321     public void onPanelNavigatedToPrefetchedSearch(boolean didResolve) {
322         long durationMs = (System.nanoTime() - mSearchRequestStartTimeNs)
323                 / TimeUtils.NANOSECONDS_PER_MILLISECOND;
324         ContextualSearchUma.logPrefetchedSearchNavigatedDuration(durationMs, didResolve);
325     }
326 
327     /**
328      * Sets the experiments to log with results seen.
329      * @param resultsSeenExperiments The experiments to log when the panel results are known.
330      */
setResultsSeenExperiments(ContextualSearchHeuristics resultsSeenExperiments)331     public void setResultsSeenExperiments(ContextualSearchHeuristics resultsSeenExperiments) {
332         mResultsSeenExperiments = resultsSeenExperiments;
333     }
334 
335     /**
336      * Sets up logging through Ranker for outcomes.
337      * @param interactionRecorder The {@link ContextualSearchInteractionRecorder} currently being
338      * used to measure to recorder user interaction outcomes.
339      */
setInteractionRecorder(ContextualSearchInteractionRecorder interactionRecorder)340     public void setInteractionRecorder(ContextualSearchInteractionRecorder interactionRecorder) {
341         mInteractionRecorder = interactionRecorder;
342         mAreOutcomesValid = false;
343     }
344 
345     /**
346      * Writes all the outcome features to the Interaction Recorder and resets it.
347      */
writeInteractionOutcomesAndReset()348     public void writeInteractionOutcomesAndReset() {
349         if (mInteractionRecorder != null && mWasActivatedByTap && mAreOutcomesValid) {
350             // Tell Ranker about the primary outcome.
351             mInteractionRecorder.logOutcome(
352                     ContextualSearchInteractionRecorder.Feature.OUTCOME_WAS_PANEL_OPENED,
353                     mWasSearchContentViewSeen);
354             ContextualSearchUma.logRankerInference(mWasSearchContentViewSeen,
355                     mInteractionRecorder.getPredictionForTapSuppression());
356             mInteractionRecorder.logOutcome(
357                     ContextualSearchInteractionRecorder.Feature.OUTCOME_WAS_CARDS_DATA_SHOWN,
358                     mWasContextualCardsDataShown);
359             if (mWasQuickActionShown) {
360                 mInteractionRecorder.logOutcome(ContextualSearchInteractionRecorder.Feature
361                                                         .OUTCOME_WAS_QUICK_ACTION_CLICKED,
362                         mWasQuickActionClicked);
363             }
364             if (mResultsSeenExperiments != null) {
365                 mResultsSeenExperiments.logRankerTapSuppressionOutcome(mInteractionRecorder);
366             }
367             mInteractionRecorder.writeLogAndReset();
368             mInteractionRecorder = null;
369         }
370     }
371 
372     /**
373      * Determine whether a new contextual search is starting.
374      * @param toState The contextual search state that will be transitioned to.
375      * @param reason The reason for the search state transition.
376      * @return Whether a new contextual search is starting.
377      */
isStartingNewContextualSearch( @anelState int toState, @StateChangeReason int reason)378     private boolean isStartingNewContextualSearch(
379             @PanelState int toState, @StateChangeReason int reason) {
380         return toState == PanelState.PEEKED
381                 && (reason == StateChangeReason.TEXT_SELECT_TAP
382                         || reason == StateChangeReason.TEXT_SELECT_LONG_PRESS);
383     }
384 
385     /**
386      * Determine whether a contextual search is ending.
387      * @param fromState The contextual search state that will be transitioned from.
388      * @param toState The contextual search state that will be transitioned to.
389      * @param isStartingSearch Whether a new contextual search is starting.
390      * @return Whether a contextual search is ending.
391      */
isEndingContextualSearch( @anelState int fromState, @PanelState int toState, boolean isStartingSearch)392     private boolean isEndingContextualSearch(
393             @PanelState int fromState, @PanelState int toState, boolean isStartingSearch) {
394         return isOngoingContextualSearch(fromState)
395                 && (toState == PanelState.CLOSED || isStartingSearch);
396     }
397 
398     /**
399      * @param fromState The state the panel is transitioning from.
400      * @return Whether there is an ongoing contextual search.
401      */
isOngoingContextualSearch(@anelState int fromState)402     private boolean isOngoingContextualSearch(@PanelState int fromState) {
403         return fromState != PanelState.UNDEFINED && fromState != PanelState.CLOSED;
404     }
405 }
406