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.DELTA_RESPONSES_CUT; 20 import static com.android.server.credentials.MetricUtilities.generateMetricKey; 21 22 import android.annotation.NonNull; 23 import android.annotation.Nullable; 24 import android.service.credentials.BeginCreateCredentialResponse; 25 import android.service.credentials.BeginGetCredentialResponse; 26 import android.service.credentials.CredentialEntry; 27 import android.util.Slog; 28 29 import com.android.server.credentials.MetricUtilities; 30 import com.android.server.credentials.metrics.shared.ResponseCollective; 31 32 import java.util.ArrayList; 33 import java.util.LinkedHashMap; 34 import java.util.List; 35 import java.util.Map; 36 37 /** 38 * Provides contextual metric collection for objects generated from 39 * {@link com.android.server.credentials.ProviderSession} flows to isolate metric 40 * collection from the core codebase. For any future additions to the ProviderSession subclass 41 * list, metric collection should be added to this file. 42 */ 43 public class ProviderSessionMetric { 44 45 private static final String TAG = "ProviderSessionMetric"; 46 47 // Specific candidate provider metric for the provider this session handles 48 @NonNull 49 protected final CandidatePhaseMetric mCandidatePhasePerProviderMetric; 50 51 // IFF there was an authentication entry clicked, this stores all required information for 52 // that event. This is for the 'get' flow. Notice these flows may be repetitive. 53 // Thus each provider stores a list of authentication metrics. The time between emits 54 // of these metrics should exceed 10 ms (given human reaction time is ~ 100's of ms), so emits 55 // will never collide. However, for aggregation, this will store information accordingly. 56 @NonNull 57 protected final List<BrowsedAuthenticationMetric> mBrowsedAuthenticationMetric = 58 new ArrayList<>(); 59 ProviderSessionMetric(int sessionIdTrackTwo)60 public ProviderSessionMetric(int sessionIdTrackTwo) { 61 mCandidatePhasePerProviderMetric = new CandidatePhaseMetric(sessionIdTrackTwo); 62 mBrowsedAuthenticationMetric.add(new BrowsedAuthenticationMetric(sessionIdTrackTwo)); 63 } 64 65 /** 66 * Retrieve the candidate provider phase metric and the data it contains. 67 */ getCandidatePhasePerProviderMetric()68 public CandidatePhaseMetric getCandidatePhasePerProviderMetric() { 69 return mCandidatePhasePerProviderMetric; 70 } 71 72 /** 73 * Retrieves the authentication clicked metric information. 74 */ getBrowsedAuthenticationMetric()75 public List<BrowsedAuthenticationMetric> getBrowsedAuthenticationMetric() { 76 return mBrowsedAuthenticationMetric; 77 } 78 79 /** 80 * This collects for ProviderSessions, with respect to the candidate providers, whether 81 * an exception occurred in the candidate call. 82 * 83 * @param hasException indicates if the candidate provider associated with an exception 84 */ collectCandidateExceptionStatus(boolean hasException)85 public void collectCandidateExceptionStatus(boolean hasException) { 86 try { 87 mCandidatePhasePerProviderMetric.setHasException(hasException); 88 } catch (Exception e) { 89 Slog.i(TAG, "Error while setting candidate metric exception " + e); 90 } 91 } 92 93 /** 94 * This collects for ProviderSessions, with respect to the authentication entry provider, 95 * if an exception occurred in the authentication entry click. It's expected that these 96 * collections always occur after at least 1 authentication metric has been collected 97 * for the provider associated with this metric encapsulation. 98 * 99 * @param hasException indicates if the candidate provider from an authentication entry 100 * associated with an exception 101 */ collectAuthenticationExceptionStatus(boolean hasException)102 public void collectAuthenticationExceptionStatus(boolean hasException) { 103 try { 104 BrowsedAuthenticationMetric mostRecentAuthenticationMetric = 105 getUsedAuthenticationMetric(); 106 mostRecentAuthenticationMetric.setHasException(hasException); 107 } catch (Exception e) { 108 Slog.i(TAG, "Error while setting authentication metric exception " + e); 109 } 110 } 111 112 /** 113 * Collects the framework only exception encountered in a candidate flow. 114 * @param exceptionType the string, cut to desired length, of the exception type 115 */ collectCandidateFrameworkException(String exceptionType)116 public void collectCandidateFrameworkException(String exceptionType) { 117 try { 118 mCandidatePhasePerProviderMetric.setFrameworkException(exceptionType); 119 } catch (Exception e) { 120 Slog.i(TAG, "Unexpected error during candidate exception metric logging: " + e); 121 } 122 } 123 collectAuthEntryUpdate(boolean isFailureStatus, boolean isCompletionStatus, int providerSessionUid)124 private void collectAuthEntryUpdate(boolean isFailureStatus, 125 boolean isCompletionStatus, int providerSessionUid) { 126 BrowsedAuthenticationMetric mostRecentAuthenticationMetric = 127 getUsedAuthenticationMetric(); 128 mostRecentAuthenticationMetric.setProviderUid(providerSessionUid); 129 if (isFailureStatus) { 130 mostRecentAuthenticationMetric.setAuthReturned(false); 131 mostRecentAuthenticationMetric.setProviderStatus( 132 ProviderStatusForMetrics.QUERY_FAILURE 133 .getMetricCode()); 134 } else if (isCompletionStatus) { 135 mostRecentAuthenticationMetric.setAuthReturned(true); 136 mostRecentAuthenticationMetric.setProviderStatus( 137 ProviderStatusForMetrics.QUERY_SUCCESS 138 .getMetricCode()); 139 } 140 } 141 getUsedAuthenticationMetric()142 private BrowsedAuthenticationMetric getUsedAuthenticationMetric() { 143 return mBrowsedAuthenticationMetric 144 .get(mBrowsedAuthenticationMetric.size() - 1); 145 } 146 147 /** 148 * Used to collect metrics at the update stage when a candidate provider gives back an update. 149 * 150 * @param isFailureStatus indicates the candidate provider sent back a terminated response 151 * @param isCompletionStatus indicates the candidate provider sent back a completion response 152 * @param providerSessionUid the uid of the provider 153 * @param isPrimary indicates if this candidate provider was the primary provider 154 */ collectCandidateMetricUpdate(boolean isFailureStatus, boolean isCompletionStatus, int providerSessionUid, boolean isAuthEntry, boolean isPrimary)155 public void collectCandidateMetricUpdate(boolean isFailureStatus, 156 boolean isCompletionStatus, int providerSessionUid, boolean isAuthEntry, 157 boolean isPrimary) { 158 try { 159 if (isAuthEntry) { 160 collectAuthEntryUpdate(isFailureStatus, isCompletionStatus, providerSessionUid); 161 return; 162 } 163 mCandidatePhasePerProviderMetric.setPrimary(isPrimary); 164 mCandidatePhasePerProviderMetric.setCandidateUid(providerSessionUid); 165 mCandidatePhasePerProviderMetric 166 .setQueryFinishTimeNanoseconds(System.nanoTime()); 167 if (isFailureStatus) { 168 mCandidatePhasePerProviderMetric.setQueryReturned(false); 169 mCandidatePhasePerProviderMetric.setProviderQueryStatus( 170 ProviderStatusForMetrics.QUERY_FAILURE 171 .getMetricCode()); 172 } else if (isCompletionStatus) { 173 mCandidatePhasePerProviderMetric.setQueryReturned(true); 174 mCandidatePhasePerProviderMetric.setProviderQueryStatus( 175 ProviderStatusForMetrics.QUERY_SUCCESS 176 .getMetricCode()); 177 } 178 } catch (Exception e) { 179 Slog.i(TAG, "Unexpected error during candidate update metric logging: " + e); 180 } 181 } 182 183 /** 184 * Starts the collection of a single provider metric in the candidate phase of the API flow. 185 * It's expected that this should be called at the start of the query phase so that session id 186 * and timestamps can be shared. They can be accessed granular-ly through the underlying 187 * objects, but for {@link com.android.server.credentials.ProviderSession} context metrics, 188 * it's recommended to use these context-specified methods. 189 * 190 * @param initMetric the pre candidate phase metric collection object of type 191 * {@link InitialPhaseMetric} used to transfer initial information 192 */ collectCandidateMetricSetupViaInitialMetric(InitialPhaseMetric initMetric)193 public void collectCandidateMetricSetupViaInitialMetric(InitialPhaseMetric initMetric) { 194 try { 195 mCandidatePhasePerProviderMetric.setServiceBeganTimeNanoseconds( 196 initMetric.getCredentialServiceStartedTimeNanoseconds()); 197 mCandidatePhasePerProviderMetric.setStartQueryTimeNanoseconds(System.nanoTime()); 198 } catch (Exception e) { 199 Slog.i(TAG, "Unexpected error during candidate setup metric logging: " + e); 200 } 201 } 202 203 /** 204 * Once candidate providers give back entries, this helps collect their info for metric 205 * purposes. 206 * 207 * @param response contains entries and data from the candidate provider responses 208 * @param isAuthEntry indicates if this is an auth entry collection or not 209 * @param initialPhaseMetric for create flows, this helps identify the response type, which 210 * will identify the *type* of create flow, especially important in 211 * track 2. This is expected to be null in get flows. 212 * @param <R> the response type associated with the API flow in progress 213 */ collectCandidateEntryMetrics(R response, boolean isAuthEntry, @Nullable InitialPhaseMetric initialPhaseMetric)214 public <R> void collectCandidateEntryMetrics(R response, boolean isAuthEntry, 215 @Nullable InitialPhaseMetric initialPhaseMetric) { 216 try { 217 if (response instanceof BeginGetCredentialResponse) { 218 beginGetCredentialResponseCollectionCandidateEntryMetrics( 219 (BeginGetCredentialResponse) response, isAuthEntry); 220 } else if (response instanceof BeginCreateCredentialResponse) { 221 beginCreateCredentialResponseCollectionCandidateEntryMetrics( 222 (BeginCreateCredentialResponse) response, initialPhaseMetric); 223 } else { 224 Slog.i(TAG, "Your response type is unsupported for candidate metric logging"); 225 } 226 } catch (Exception e) { 227 Slog.i(TAG, "Unexpected error during candidate entry metric logging: " + e); 228 } 229 } 230 231 /** 232 * Once entries are received from the registry, this helps collect their info for metric 233 * purposes. 234 * 235 * @param entries contains matching entries from the Credential Registry. 236 */ collectCandidateEntryMetrics(List<CredentialEntry> entries)237 public void collectCandidateEntryMetrics(List<CredentialEntry> entries) { 238 int numCredEntries = entries.size(); 239 int numRemoteEntry = MetricUtilities.ZERO; 240 int numActionEntries = MetricUtilities.ZERO; 241 int numAuthEntries = MetricUtilities.ZERO; 242 Map<EntryEnum, Integer> entryCounts = new LinkedHashMap<>(); 243 Map<String, Integer> responseCounts = new LinkedHashMap<>(); 244 entryCounts.put(EntryEnum.REMOTE_ENTRY, numRemoteEntry); 245 entryCounts.put(EntryEnum.CREDENTIAL_ENTRY, numCredEntries); 246 entryCounts.put(EntryEnum.ACTION_ENTRY, numActionEntries); 247 entryCounts.put(EntryEnum.AUTHENTICATION_ENTRY, numAuthEntries); 248 249 entries.forEach(entry -> { 250 String entryKey = generateMetricKey(entry.getType(), DELTA_RESPONSES_CUT); 251 responseCounts.put(entryKey, responseCounts.getOrDefault(entryKey, 0) + 1); 252 }); 253 ResponseCollective responseCollective = new ResponseCollective(responseCounts, entryCounts); 254 mCandidatePhasePerProviderMetric.setResponseCollective(responseCollective); 255 } 256 257 /** 258 * This sets up an authentication metric collector to the flow. This must be called before 259 * any logical edits are done in a new authentication entry metric collection. 260 */ createAuthenticationBrowsingMetric()261 public void createAuthenticationBrowsingMetric() { 262 BrowsedAuthenticationMetric browsedAuthenticationMetric = 263 new BrowsedAuthenticationMetric(mCandidatePhasePerProviderMetric 264 .getSessionIdProvider()); 265 mBrowsedAuthenticationMetric.add(browsedAuthenticationMetric); 266 } 267 beginCreateCredentialResponseCollectionCandidateEntryMetrics( BeginCreateCredentialResponse response, InitialPhaseMetric initialPhaseMetric)268 private void beginCreateCredentialResponseCollectionCandidateEntryMetrics( 269 BeginCreateCredentialResponse response, InitialPhaseMetric initialPhaseMetric) { 270 Map<EntryEnum, Integer> entryCounts = new LinkedHashMap<>(); 271 var createEntries = response.getCreateEntries(); 272 int numRemoteEntry = response.getRemoteCreateEntry() == null ? MetricUtilities.ZERO : 273 MetricUtilities.UNIT; 274 int numCreateEntries = createEntries.size(); 275 entryCounts.put(EntryEnum.REMOTE_ENTRY, numRemoteEntry); 276 entryCounts.put(EntryEnum.CREDENTIAL_ENTRY, numCreateEntries); 277 278 Map<String, Integer> responseCounts = new LinkedHashMap<>(); 279 String[] requestStrings = initialPhaseMetric == null ? new String[0] : 280 initialPhaseMetric.getUniqueRequestStrings(); 281 if (requestStrings.length > 0) { 282 responseCounts.put(requestStrings[0], initialPhaseMetric.getUniqueRequestCounts()[0]); 283 } 284 285 ResponseCollective responseCollective = new ResponseCollective(responseCounts, entryCounts); 286 mCandidatePhasePerProviderMetric.setResponseCollective(responseCollective); 287 } 288 beginGetCredentialResponseCollectionCandidateEntryMetrics( BeginGetCredentialResponse response, boolean isAuthEntry)289 private void beginGetCredentialResponseCollectionCandidateEntryMetrics( 290 BeginGetCredentialResponse response, boolean isAuthEntry) { 291 Map<EntryEnum, Integer> entryCounts = new LinkedHashMap<>(); 292 Map<String, Integer> responseCounts = new LinkedHashMap<>(); 293 int numCredEntries = response.getCredentialEntries().size(); 294 int numActionEntries = response.getActions().size(); 295 int numAuthEntries = response.getAuthenticationActions().size(); 296 int numRemoteEntry = response.getRemoteCredentialEntry() != null ? MetricUtilities.ZERO : 297 MetricUtilities.UNIT; 298 entryCounts.put(EntryEnum.REMOTE_ENTRY, numRemoteEntry); 299 entryCounts.put(EntryEnum.CREDENTIAL_ENTRY, numCredEntries); 300 entryCounts.put(EntryEnum.ACTION_ENTRY, numActionEntries); 301 entryCounts.put(EntryEnum.AUTHENTICATION_ENTRY, numAuthEntries); 302 303 response.getCredentialEntries().forEach(entry -> { 304 String entryKey = generateMetricKey(entry.getType(), DELTA_RESPONSES_CUT); 305 responseCounts.put(entryKey, responseCounts.getOrDefault(entryKey, 0) + 1); 306 }); 307 308 ResponseCollective responseCollective = new ResponseCollective(responseCounts, entryCounts); 309 310 if (!isAuthEntry) { 311 mCandidatePhasePerProviderMetric.setResponseCollective(responseCollective); 312 } else { 313 // The most recent auth entry must be created already 314 var browsedAuthenticationMetric = 315 mBrowsedAuthenticationMetric.get(mBrowsedAuthenticationMetric.size() - 1); 316 browsedAuthenticationMetric.setAuthEntryCollective(responseCollective); 317 } 318 } 319 } 320