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