1 /* 2 * Copyright (C) 2023 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.server.credentials.metrics; 18 19 import static com.android.server.credentials.MetricUtilities.DEFAULT_INT_32; 20 import static com.android.server.credentials.MetricUtilities.DELTA_EXCEPTION_CUT; 21 import static com.android.server.credentials.MetricUtilities.DELTA_RESPONSES_CUT; 22 import static com.android.server.credentials.MetricUtilities.generateMetricKey; 23 import static com.android.server.credentials.MetricUtilities.logApiCalledAggregateCandidate; 24 import static com.android.server.credentials.MetricUtilities.logApiCalledAuthenticationMetric; 25 import static com.android.server.credentials.MetricUtilities.logApiCalledCandidateGetMetric; 26 import static com.android.server.credentials.MetricUtilities.logApiCalledCandidatePhase; 27 import static com.android.server.credentials.MetricUtilities.logApiCalledFinalPhase; 28 import static com.android.server.credentials.MetricUtilities.logApiCalledNoUidFinal; 29 import static com.android.server.credentials.metrics.ApiName.GET_CREDENTIAL; 30 import static com.android.server.credentials.metrics.ApiName.GET_CREDENTIAL_VIA_REGISTRY; 31 32 import android.annotation.NonNull; 33 import android.content.ComponentName; 34 import android.credentials.CreateCredentialRequest; 35 import android.credentials.GetCredentialRequest; 36 import android.credentials.ui.UserSelectionDialogResult; 37 import android.util.Slog; 38 39 import com.android.server.credentials.MetricUtilities; 40 import com.android.server.credentials.ProviderSession; 41 42 import java.util.ArrayList; 43 import java.util.LinkedHashMap; 44 import java.util.List; 45 import java.util.Map; 46 47 /** 48 * Provides contextual metric collection for objects generated from classes such as 49 * {@link com.android.server.credentials.GetRequestSession}, 50 * {@link com.android.server.credentials.CreateRequestSession}, 51 * and {@link com.android.server.credentials.ClearRequestSession} flows to isolate metric 52 * collection from the core codebase. For any future additions to the RequestSession subclass 53 * list, metric collection should be added to this file. 54 */ 55 public class RequestSessionMetric { 56 private static final String TAG = "RequestSessionMetric"; 57 58 // As emits occur in sequential order, increment this counter and utilize 59 protected int mSequenceCounter = 0; 60 61 protected final InitialPhaseMetric mInitialPhaseMetric; 62 protected final ChosenProviderFinalPhaseMetric 63 mChosenProviderFinalPhaseMetric; 64 protected List<CandidateBrowsingPhaseMetric> mCandidateBrowsingPhaseMetric = new ArrayList<>(); 65 // Specific aggregate candidate provider metric for the provider this session handles 66 @NonNull 67 protected final CandidateAggregateMetric mCandidateAggregateMetric; 68 // Since track two is shared, this allows provider sessions to capture a metric-specific 69 // session token for the flow where the provider is known 70 private final int mSessionIdTrackTwo; 71 RequestSessionMetric(int sessionIdTrackOne, int sessionIdTrackTwo)72 public RequestSessionMetric(int sessionIdTrackOne, int sessionIdTrackTwo) { 73 mSessionIdTrackTwo = sessionIdTrackTwo; 74 mInitialPhaseMetric = new InitialPhaseMetric(sessionIdTrackOne); 75 mCandidateAggregateMetric = new CandidateAggregateMetric(sessionIdTrackOne); 76 mChosenProviderFinalPhaseMetric = new ChosenProviderFinalPhaseMetric( 77 sessionIdTrackOne, sessionIdTrackTwo); 78 } 79 80 /** 81 * Increments the metric emit sequence counter and returns the current state value of the 82 * sequence. 83 * 84 * @return the current state value of the metric emit sequence. 85 */ returnIncrementSequence()86 public int returnIncrementSequence() { 87 return ++mSequenceCounter; 88 } 89 90 91 /** 92 * @return the initial metrics associated with the request session 93 */ getInitialPhaseMetric()94 public InitialPhaseMetric getInitialPhaseMetric() { 95 return mInitialPhaseMetric; 96 } 97 98 /** 99 * @return the aggregate candidate phase metrics associated with the request session 100 */ getCandidateAggregateMetric()101 public CandidateAggregateMetric getCandidateAggregateMetric() { 102 return mCandidateAggregateMetric; 103 } 104 105 /** 106 * Upon starting the service, this fills the initial phase metric properly. 107 * 108 * @param timestampStarted the timestamp the service begins at 109 * @param mCallingUid the calling process's uid 110 * @param metricCode typically pulled from {@link ApiName} 111 */ collectInitialPhaseMetricInfo(long timestampStarted, int mCallingUid, int metricCode)112 public void collectInitialPhaseMetricInfo(long timestampStarted, 113 int mCallingUid, int metricCode) { 114 try { 115 mInitialPhaseMetric.setCredentialServiceStartedTimeNanoseconds(timestampStarted); 116 mInitialPhaseMetric.setCallerUid(mCallingUid); 117 mInitialPhaseMetric.setApiName(metricCode); 118 } catch (Exception e) { 119 Slog.i(TAG, "Unexpected error collecting initial phase metric start info: " + e); 120 } 121 } 122 123 /** 124 * Collects whether the UI returned for metric purposes. 125 * 126 * @param uiReturned indicates whether the ui returns or not 127 */ collectUiReturnedFinalPhase(boolean uiReturned)128 public void collectUiReturnedFinalPhase(boolean uiReturned) { 129 try { 130 mChosenProviderFinalPhaseMetric.setUiReturned(uiReturned); 131 } catch (Exception e) { 132 Slog.i(TAG, "Unexpected error collecting ui end time metric: " + e); 133 } 134 } 135 136 /** 137 * Sets the start time for the UI being called for metric purposes. 138 * 139 * @param uiCallStartTime the nanosecond time when the UI call began 140 */ collectUiCallStartTime(long uiCallStartTime)141 public void collectUiCallStartTime(long uiCallStartTime) { 142 try { 143 mChosenProviderFinalPhaseMetric.setUiCallStartTimeNanoseconds(uiCallStartTime); 144 } catch (Exception e) { 145 Slog.i(TAG, "Unexpected error collecting ui start metric: " + e); 146 } 147 } 148 149 /** 150 * When the UI responds to the framework at the very final phase, this collects the timestamp 151 * and status of the return for metric purposes. 152 * 153 * @param uiReturned indicates whether the ui returns or not 154 * @param uiEndTimestamp the nanosecond time when the UI call ended 155 */ collectUiResponseData(boolean uiReturned, long uiEndTimestamp)156 public void collectUiResponseData(boolean uiReturned, long uiEndTimestamp) { 157 try { 158 mChosenProviderFinalPhaseMetric.setUiReturned(uiReturned); 159 mChosenProviderFinalPhaseMetric.setUiCallEndTimeNanoseconds(uiEndTimestamp); 160 } catch (Exception e) { 161 Slog.i(TAG, "Unexpected error collecting ui response metric: " + e); 162 } 163 } 164 165 /** 166 * Collects the final chosen provider status, with the status value coming from 167 * {@link ApiStatus}. 168 * 169 * @param status the final status of the chosen provider 170 */ collectChosenProviderStatus(int status)171 public void collectChosenProviderStatus(int status) { 172 try { 173 mChosenProviderFinalPhaseMetric.setChosenProviderStatus(status); 174 } catch (Exception e) { 175 Slog.i(TAG, "Unexpected error setting chosen provider status metric: " + e); 176 } 177 } 178 179 /** 180 * Collects initializations for Create flow metrics. 181 * 182 * @param origin indicates if an origin was passed in or not 183 */ collectCreateFlowInitialMetricInfo(boolean origin, CreateCredentialRequest request)184 public void collectCreateFlowInitialMetricInfo(boolean origin, 185 CreateCredentialRequest request) { 186 try { 187 mInitialPhaseMetric.setOriginSpecified(origin); 188 mInitialPhaseMetric.setRequestCounts(Map.of(generateMetricKey(request.getType(), 189 DELTA_RESPONSES_CUT), MetricUtilities.UNIT)); 190 } catch (Exception e) { 191 Slog.i(TAG, "Unexpected error collecting create flow metric: " + e); 192 } 193 } 194 195 // Used by get flows to generate the unique request count maps getRequestCountMap(GetCredentialRequest request)196 private Map<String, Integer> getRequestCountMap(GetCredentialRequest request) { 197 Map<String, Integer> uniqueRequestCounts = new LinkedHashMap<>(); 198 try { 199 request.getCredentialOptions().forEach(option -> { 200 String optionKey = generateMetricKey(option.getType(), DELTA_RESPONSES_CUT); 201 uniqueRequestCounts.put(optionKey, uniqueRequestCounts.getOrDefault(optionKey, 202 0) + 1); 203 }); 204 } catch (Exception e) { 205 Slog.i(TAG, "Unexpected error during get request count map metric logging: " + e); 206 } 207 return uniqueRequestCounts; 208 } 209 210 /** 211 * Collects initializations for Get flow metrics. 212 * 213 * @param request the get credential request containing information to parse for metrics 214 */ collectGetFlowInitialMetricInfo(GetCredentialRequest request)215 public void collectGetFlowInitialMetricInfo(GetCredentialRequest request) { 216 try { 217 mInitialPhaseMetric.setOriginSpecified(request.getOrigin() != null); 218 mInitialPhaseMetric.setRequestCounts(getRequestCountMap(request)); 219 } catch (Exception e) { 220 Slog.i(TAG, "Unexpected error collecting get flow initial metric: " + e); 221 } 222 } 223 224 /** 225 * During browsing, where multiple entries can be selected, this collects the browsing phase 226 * metric information. This is emitted together with the final phase, and the recursive path 227 * with authentication entries, which may occur in rare circumstances, are captured. 228 * 229 * @param selection contains the selected entry key type 230 * @param selectedProviderPhaseMetric contains the utility information of the selected provider 231 */ collectMetricPerBrowsingSelect(UserSelectionDialogResult selection, CandidatePhaseMetric selectedProviderPhaseMetric)232 public void collectMetricPerBrowsingSelect(UserSelectionDialogResult selection, 233 CandidatePhaseMetric selectedProviderPhaseMetric) { 234 try { 235 CandidateBrowsingPhaseMetric browsingPhaseMetric = new CandidateBrowsingPhaseMetric(); 236 browsingPhaseMetric.setEntryEnum( 237 EntryEnum.getMetricCodeFromString(selection.getEntryKey())); 238 browsingPhaseMetric.setProviderUid(selectedProviderPhaseMetric.getCandidateUid()); 239 mCandidateBrowsingPhaseMetric.add(browsingPhaseMetric); 240 } catch (Exception e) { 241 Slog.i(TAG, "Unexpected error collecting browsing metric: " + e); 242 } 243 } 244 245 /** 246 * Updates the final phase metric with the designated bit. 247 * 248 * @param exceptionBitFinalPhase represents if the final phase provider had an exception 249 */ setHasExceptionFinalPhase(boolean exceptionBitFinalPhase)250 private void setHasExceptionFinalPhase(boolean exceptionBitFinalPhase) { 251 try { 252 mChosenProviderFinalPhaseMetric.setHasException(exceptionBitFinalPhase); 253 } catch (Exception e) { 254 Slog.i(TAG, "Unexpected error setting final exception metric: " + e); 255 } 256 } 257 258 /** 259 * This allows collecting the framework exception string for the final phase metric. 260 * NOTE that this exception will be cut for space optimizations. 261 * 262 * @param exception the framework exception that is being recorded 263 */ collectFrameworkException(String exception)264 public void collectFrameworkException(String exception) { 265 try { 266 mChosenProviderFinalPhaseMetric.setFrameworkException( 267 generateMetricKey(exception, DELTA_EXCEPTION_CUT)); 268 } catch (Exception e) { 269 Slog.w(TAG, "Unexpected error during metric logging: " + e); 270 } 271 } 272 273 /** 274 * Allows encapsulating the overall final phase metric status from the chosen and final 275 * provider. 276 * 277 * @param hasException represents if the final phase provider had an exception 278 * @param finalStatus represents the final status of the chosen provider 279 */ collectFinalPhaseProviderMetricStatus(boolean hasException, ProviderStatusForMetrics finalStatus)280 public void collectFinalPhaseProviderMetricStatus(boolean hasException, 281 ProviderStatusForMetrics finalStatus) { 282 try { 283 mChosenProviderFinalPhaseMetric.setHasException(hasException); 284 mChosenProviderFinalPhaseMetric.setChosenProviderStatus( 285 finalStatus.getMetricCode()); 286 } catch (Exception e) { 287 Slog.i(TAG, "Unexpected error during final phase provider status metric logging: " + e); 288 } 289 } 290 291 /** 292 * Used to update metrics when a response is received in a RequestSession. 293 * 294 * @param componentName the component name associated with the provider the response is for 295 */ updateMetricsOnResponseReceived(Map<String, ProviderSession> providers, ComponentName componentName, boolean isPrimary)296 public void updateMetricsOnResponseReceived(Map<String, ProviderSession> providers, 297 ComponentName componentName, boolean isPrimary) { 298 try { 299 var chosenProviderSession = providers.get(componentName.flattenToString()); 300 if (chosenProviderSession != null) { 301 ProviderSessionMetric providerSessionMetric = 302 chosenProviderSession.getProviderSessionMetric(); 303 collectChosenMetricViaCandidateTransfer(providerSessionMetric 304 .getCandidatePhasePerProviderMetric(), isPrimary); 305 } 306 } catch (Exception e) { 307 Slog.i(TAG, "Exception upon candidate to chosen metric transfer: " + e); 308 } 309 } 310 311 /** 312 * Called by RequestSessions upon chosen metric determination. It's expected that most bits 313 * are transferred here. However, certain new information, such as the selected provider's final 314 * exception bit, the framework to ui and back latency, or the ui response bit are set at other 315 * locations. Other information, such browsing metrics, api_status, and the sequence id count 316 * are combined during the final emit moment with the actual and official 317 * {@link com.android.internal.util.FrameworkStatsLog} metric generation. 318 * 319 * @param candidatePhaseMetric the componentName to associate with a provider 320 * @param isPrimary indicates that this chosen provider is the primary provider (or not) 321 */ collectChosenMetricViaCandidateTransfer(CandidatePhaseMetric candidatePhaseMetric, boolean isPrimary)322 public void collectChosenMetricViaCandidateTransfer(CandidatePhaseMetric candidatePhaseMetric, 323 boolean isPrimary) { 324 try { 325 mChosenProviderFinalPhaseMetric.setChosenUid(candidatePhaseMetric.getCandidateUid()); 326 mChosenProviderFinalPhaseMetric.setPrimary(isPrimary); 327 328 mChosenProviderFinalPhaseMetric.setQueryPhaseLatencyMicroseconds( 329 candidatePhaseMetric.getQueryLatencyMicroseconds()); 330 331 mChosenProviderFinalPhaseMetric.setServiceBeganTimeNanoseconds( 332 candidatePhaseMetric.getServiceBeganTimeNanoseconds()); 333 mChosenProviderFinalPhaseMetric.setQueryStartTimeNanoseconds( 334 candidatePhaseMetric.getStartQueryTimeNanoseconds()); 335 mChosenProviderFinalPhaseMetric.setQueryEndTimeNanoseconds(candidatePhaseMetric 336 .getQueryFinishTimeNanoseconds()); 337 mChosenProviderFinalPhaseMetric.setResponseCollective( 338 candidatePhaseMetric.getResponseCollective()); 339 mChosenProviderFinalPhaseMetric.setFinalFinishTimeNanoseconds(System.nanoTime()); 340 } catch (Exception e) { 341 Slog.i(TAG, "Unexpected error during metric candidate to final transfer: " + e); 342 } 343 } 344 345 /** 346 * In the final phase, this helps log use cases that were either pure failures or user 347 * canceled. It's expected that {@link #collectFinalPhaseProviderMetricStatus(boolean, 348 * ProviderStatusForMetrics) collectFinalPhaseProviderMetricStatus} is called prior to this. 349 * Otherwise, the logging will miss required bits. 350 * 351 * @param isUserCanceledError a boolean indicating if the error was due to user cancelling 352 */ logFailureOrUserCancel(boolean isUserCanceledError)353 public void logFailureOrUserCancel(boolean isUserCanceledError) { 354 try { 355 if (isUserCanceledError) { 356 setHasExceptionFinalPhase(/* has_exception */ false); 357 logApiCalledAtFinish( 358 /* apiStatus */ ApiStatus.USER_CANCELED.getMetricCode()); 359 } else { 360 logApiCalledAtFinish( 361 /* apiStatus */ ApiStatus.FAILURE.getMetricCode()); 362 } 363 } catch (Exception e) { 364 Slog.i(TAG, "Unexpected error during final metric failure emit: " + e); 365 } 366 } 367 368 /** 369 * Handles candidate phase metric emit in the RequestSession context, after the candidate phase 370 * completes. 371 * 372 * @param providers a map with known providers and their held metric objects 373 */ logCandidatePhaseMetrics(Map<String, ProviderSession> providers)374 public void logCandidatePhaseMetrics(Map<String, ProviderSession> providers) { 375 try { 376 logApiCalledCandidatePhase(providers, ++mSequenceCounter, mInitialPhaseMetric); 377 if (mInitialPhaseMetric.getApiName() == GET_CREDENTIAL.getMetricCode() 378 || mInitialPhaseMetric.getApiName() == GET_CREDENTIAL_VIA_REGISTRY 379 .getMetricCode()) { 380 logApiCalledCandidateGetMetric(providers, mSequenceCounter); 381 } 382 } catch (Exception e) { 383 Slog.i(TAG, "Unexpected error during candidate metric emit: " + e); 384 } 385 } 386 387 /** 388 * Handles aggregate candidate phase metric emits in the RequestSession context, after the 389 * candidate phase completes. 390 * 391 * @param providers a map with known providers and their held metric objects 392 */ logCandidateAggregateMetrics(Map<String, ProviderSession> providers)393 public void logCandidateAggregateMetrics(Map<String, ProviderSession> providers) { 394 try { 395 mCandidateAggregateMetric.collectAverages(providers); 396 logApiCalledAggregateCandidate(mCandidateAggregateMetric, ++mSequenceCounter); 397 } catch (Exception e) { 398 Slog.i(TAG, "Unexpected error during aggregate candidate logging " + e); 399 } 400 } 401 402 /** 403 * This logs the authentication entry when browsed. Combined with the known browsed clicks 404 * in the {@link ChosenProviderFinalPhaseMetric}, this fully captures the authentication entry 405 * logic for multiple loops. An auth entry may have default or missing data, but if a provider 406 * was never assigned to an auth entry, this indicates an auth entry was never clicked. 407 * This case is handled in this emit. 408 * 409 * @param browsedAuthenticationMetric the authentication metric information to emit 410 */ logAuthEntry(BrowsedAuthenticationMetric browsedAuthenticationMetric)411 public void logAuthEntry(BrowsedAuthenticationMetric browsedAuthenticationMetric) { 412 try { 413 if (browsedAuthenticationMetric.getProviderUid() == DEFAULT_INT_32) { 414 Slog.v(TAG, "An authentication entry was not clicked"); 415 return; 416 } 417 logApiCalledAuthenticationMetric(browsedAuthenticationMetric, ++mSequenceCounter); 418 } catch (Exception e) { 419 Slog.i(TAG, "Unexpected error during auth entry metric emit: " + e); 420 } 421 422 } 423 424 /** 425 * Handles the final logging for RequestSession context for the final phase. 426 * 427 * @param apiStatus the final status of the api being called 428 */ logApiCalledAtFinish(int apiStatus)429 public void logApiCalledAtFinish(int apiStatus) { 430 try { 431 logApiCalledFinalPhase(mChosenProviderFinalPhaseMetric, mCandidateBrowsingPhaseMetric, 432 apiStatus, 433 ++mSequenceCounter); 434 logApiCalledNoUidFinal(mChosenProviderFinalPhaseMetric, mCandidateBrowsingPhaseMetric, 435 apiStatus, 436 ++mSequenceCounter); 437 } catch (Exception e) { 438 Slog.i(TAG, "Unexpected error during final metric emit: " + e); 439 } 440 } 441 getSessionIdTrackTwo()442 public int getSessionIdTrackTwo() { 443 return mSessionIdTrackTwo; 444 } 445 } 446