blob: 83b57c4f78c26332372c5eb47e0756570a4c9761 [file] [log] [blame]
/*
* Copyright (C) 2023 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.server.credentials.metrics;
import static com.android.server.credentials.MetricUtilities.DEFAULT_INT_32;
import static com.android.server.credentials.MetricUtilities.DELTA_EXCEPTION_CUT;
import static com.android.server.credentials.MetricUtilities.DELTA_RESPONSES_CUT;
import static com.android.server.credentials.MetricUtilities.generateMetricKey;
import static com.android.server.credentials.MetricUtilities.logApiCalledAggregateCandidate;
import static com.android.server.credentials.MetricUtilities.logApiCalledAuthenticationMetric;
import static com.android.server.credentials.MetricUtilities.logApiCalledCandidateGetMetric;
import static com.android.server.credentials.MetricUtilities.logApiCalledCandidatePhase;
import static com.android.server.credentials.MetricUtilities.logApiCalledFinalPhase;
import static com.android.server.credentials.MetricUtilities.logApiCalledNoUidFinal;
import static com.android.server.credentials.metrics.ApiName.GET_CREDENTIAL;
import static com.android.server.credentials.metrics.ApiName.GET_CREDENTIAL_VIA_REGISTRY;
import android.annotation.NonNull;
import android.content.ComponentName;
import android.credentials.CreateCredentialRequest;
import android.credentials.GetCredentialRequest;
import android.credentials.ui.UserSelectionDialogResult;
import android.util.Slog;
import com.android.server.credentials.MetricUtilities;
import com.android.server.credentials.ProviderSession;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
/**
* Provides contextual metric collection for objects generated from classes such as
* {@link com.android.server.credentials.GetRequestSession},
* {@link com.android.server.credentials.CreateRequestSession},
* and {@link com.android.server.credentials.ClearRequestSession} flows to isolate metric
* collection from the core codebase. For any future additions to the RequestSession subclass
* list, metric collection should be added to this file.
*/
public class RequestSessionMetric {
private static final String TAG = "RequestSessionMetric";
// As emits occur in sequential order, increment this counter and utilize
protected int mSequenceCounter = 0;
protected final InitialPhaseMetric mInitialPhaseMetric;
protected final ChosenProviderFinalPhaseMetric
mChosenProviderFinalPhaseMetric;
protected List<CandidateBrowsingPhaseMetric> mCandidateBrowsingPhaseMetric = new ArrayList<>();
// Specific aggregate candidate provider metric for the provider this session handles
@NonNull
protected final CandidateAggregateMetric mCandidateAggregateMetric;
// Since track two is shared, this allows provider sessions to capture a metric-specific
// session token for the flow where the provider is known
private final int mSessionIdTrackTwo;
public RequestSessionMetric(int sessionIdTrackOne, int sessionIdTrackTwo) {
mSessionIdTrackTwo = sessionIdTrackTwo;
mInitialPhaseMetric = new InitialPhaseMetric(sessionIdTrackOne);
mCandidateAggregateMetric = new CandidateAggregateMetric(sessionIdTrackOne);
mChosenProviderFinalPhaseMetric = new ChosenProviderFinalPhaseMetric(
sessionIdTrackOne, sessionIdTrackTwo);
}
/**
* Increments the metric emit sequence counter and returns the current state value of the
* sequence.
*
* @return the current state value of the metric emit sequence.
*/
public int returnIncrementSequence() {
return ++mSequenceCounter;
}
/**
* @return the initial metrics associated with the request session
*/
public InitialPhaseMetric getInitialPhaseMetric() {
return mInitialPhaseMetric;
}
/**
* @return the aggregate candidate phase metrics associated with the request session
*/
public CandidateAggregateMetric getCandidateAggregateMetric() {
return mCandidateAggregateMetric;
}
/**
* Upon starting the service, this fills the initial phase metric properly.
*
* @param timestampStarted the timestamp the service begins at
* @param mCallingUid the calling process's uid
* @param metricCode typically pulled from {@link ApiName}
*/
public void collectInitialPhaseMetricInfo(long timestampStarted,
int mCallingUid, int metricCode) {
try {
mInitialPhaseMetric.setCredentialServiceStartedTimeNanoseconds(timestampStarted);
mInitialPhaseMetric.setCallerUid(mCallingUid);
mInitialPhaseMetric.setApiName(metricCode);
} catch (Exception e) {
Slog.i(TAG, "Unexpected error collecting initial phase metric start info: " + e);
}
}
/**
* Collects whether the UI returned for metric purposes.
*
* @param uiReturned indicates whether the ui returns or not
*/
public void collectUiReturnedFinalPhase(boolean uiReturned) {
try {
mChosenProviderFinalPhaseMetric.setUiReturned(uiReturned);
} catch (Exception e) {
Slog.i(TAG, "Unexpected error collecting ui end time metric: " + e);
}
}
/**
* Sets the start time for the UI being called for metric purposes.
*
* @param uiCallStartTime the nanosecond time when the UI call began
*/
public void collectUiCallStartTime(long uiCallStartTime) {
try {
mChosenProviderFinalPhaseMetric.setUiCallStartTimeNanoseconds(uiCallStartTime);
} catch (Exception e) {
Slog.i(TAG, "Unexpected error collecting ui start metric: " + e);
}
}
/**
* When the UI responds to the framework at the very final phase, this collects the timestamp
* and status of the return for metric purposes.
*
* @param uiReturned indicates whether the ui returns or not
* @param uiEndTimestamp the nanosecond time when the UI call ended
*/
public void collectUiResponseData(boolean uiReturned, long uiEndTimestamp) {
try {
mChosenProviderFinalPhaseMetric.setUiReturned(uiReturned);
mChosenProviderFinalPhaseMetric.setUiCallEndTimeNanoseconds(uiEndTimestamp);
} catch (Exception e) {
Slog.i(TAG, "Unexpected error collecting ui response metric: " + e);
}
}
/**
* Collects the final chosen provider status, with the status value coming from
* {@link ApiStatus}.
*
* @param status the final status of the chosen provider
*/
public void collectChosenProviderStatus(int status) {
try {
mChosenProviderFinalPhaseMetric.setChosenProviderStatus(status);
} catch (Exception e) {
Slog.i(TAG, "Unexpected error setting chosen provider status metric: " + e);
}
}
/**
* Collects initializations for Create flow metrics.
*
* @param origin indicates if an origin was passed in or not
*/
public void collectCreateFlowInitialMetricInfo(boolean origin,
CreateCredentialRequest request) {
try {
mInitialPhaseMetric.setOriginSpecified(origin);
mInitialPhaseMetric.setRequestCounts(Map.of(generateMetricKey(request.getType(),
DELTA_RESPONSES_CUT), MetricUtilities.UNIT));
} catch (Exception e) {
Slog.i(TAG, "Unexpected error collecting create flow metric: " + e);
}
}
// Used by get flows to generate the unique request count maps
private Map<String, Integer> getRequestCountMap(GetCredentialRequest request) {
Map<String, Integer> uniqueRequestCounts = new LinkedHashMap<>();
try {
request.getCredentialOptions().forEach(option -> {
String optionKey = generateMetricKey(option.getType(), DELTA_RESPONSES_CUT);
uniqueRequestCounts.put(optionKey, uniqueRequestCounts.getOrDefault(optionKey,
0) + 1);
});
} catch (Exception e) {
Slog.i(TAG, "Unexpected error during get request count map metric logging: " + e);
}
return uniqueRequestCounts;
}
/**
* Collects initializations for Get flow metrics.
*
* @param request the get credential request containing information to parse for metrics
*/
public void collectGetFlowInitialMetricInfo(GetCredentialRequest request) {
try {
mInitialPhaseMetric.setOriginSpecified(request.getOrigin() != null);
mInitialPhaseMetric.setRequestCounts(getRequestCountMap(request));
} catch (Exception e) {
Slog.i(TAG, "Unexpected error collecting get flow initial metric: " + e);
}
}
/**
* During browsing, where multiple entries can be selected, this collects the browsing phase
* metric information. This is emitted together with the final phase, and the recursive path
* with authentication entries, which may occur in rare circumstances, are captured.
*
* @param selection contains the selected entry key type
* @param selectedProviderPhaseMetric contains the utility information of the selected provider
*/
public void collectMetricPerBrowsingSelect(UserSelectionDialogResult selection,
CandidatePhaseMetric selectedProviderPhaseMetric) {
try {
CandidateBrowsingPhaseMetric browsingPhaseMetric = new CandidateBrowsingPhaseMetric();
browsingPhaseMetric.setEntryEnum(
EntryEnum.getMetricCodeFromString(selection.getEntryKey()));
browsingPhaseMetric.setProviderUid(selectedProviderPhaseMetric.getCandidateUid());
mCandidateBrowsingPhaseMetric.add(browsingPhaseMetric);
} catch (Exception e) {
Slog.i(TAG, "Unexpected error collecting browsing metric: " + e);
}
}
/**
* Updates the final phase metric with the designated bit.
*
* @param exceptionBitFinalPhase represents if the final phase provider had an exception
*/
private void setHasExceptionFinalPhase(boolean exceptionBitFinalPhase) {
try {
mChosenProviderFinalPhaseMetric.setHasException(exceptionBitFinalPhase);
} catch (Exception e) {
Slog.i(TAG, "Unexpected error setting final exception metric: " + e);
}
}
/**
* This allows collecting the framework exception string for the final phase metric.
* NOTE that this exception will be cut for space optimizations.
*
* @param exception the framework exception that is being recorded
*/
public void collectFrameworkException(String exception) {
try {
mChosenProviderFinalPhaseMetric.setFrameworkException(
generateMetricKey(exception, DELTA_EXCEPTION_CUT));
} catch (Exception e) {
Slog.w(TAG, "Unexpected error during metric logging: " + e);
}
}
/**
* Allows encapsulating the overall final phase metric status from the chosen and final
* provider.
*
* @param hasException represents if the final phase provider had an exception
* @param finalStatus represents the final status of the chosen provider
*/
public void collectFinalPhaseProviderMetricStatus(boolean hasException,
ProviderStatusForMetrics finalStatus) {
try {
mChosenProviderFinalPhaseMetric.setHasException(hasException);
mChosenProviderFinalPhaseMetric.setChosenProviderStatus(
finalStatus.getMetricCode());
} catch (Exception e) {
Slog.i(TAG, "Unexpected error during final phase provider status metric logging: " + e);
}
}
/**
* Used to update metrics when a response is received in a RequestSession.
*
* @param componentName the component name associated with the provider the response is for
*/
public void updateMetricsOnResponseReceived(Map<String, ProviderSession> providers,
ComponentName componentName, boolean isPrimary) {
try {
var chosenProviderSession = providers.get(componentName.flattenToString());
if (chosenProviderSession != null) {
ProviderSessionMetric providerSessionMetric =
chosenProviderSession.getProviderSessionMetric();
collectChosenMetricViaCandidateTransfer(providerSessionMetric
.getCandidatePhasePerProviderMetric(), isPrimary);
}
} catch (Exception e) {
Slog.i(TAG, "Exception upon candidate to chosen metric transfer: " + e);
}
}
/**
* Called by RequestSessions upon chosen metric determination. It's expected that most bits
* are transferred here. However, certain new information, such as the selected provider's final
* exception bit, the framework to ui and back latency, or the ui response bit are set at other
* locations. Other information, such browsing metrics, api_status, and the sequence id count
* are combined during the final emit moment with the actual and official
* {@link com.android.internal.util.FrameworkStatsLog} metric generation.
*
* @param candidatePhaseMetric the componentName to associate with a provider
* @param isPrimary indicates that this chosen provider is the primary provider (or not)
*/
public void collectChosenMetricViaCandidateTransfer(CandidatePhaseMetric candidatePhaseMetric,
boolean isPrimary) {
try {
mChosenProviderFinalPhaseMetric.setChosenUid(candidatePhaseMetric.getCandidateUid());
mChosenProviderFinalPhaseMetric.setPrimary(isPrimary);
mChosenProviderFinalPhaseMetric.setQueryPhaseLatencyMicroseconds(
candidatePhaseMetric.getQueryLatencyMicroseconds());
mChosenProviderFinalPhaseMetric.setServiceBeganTimeNanoseconds(
candidatePhaseMetric.getServiceBeganTimeNanoseconds());
mChosenProviderFinalPhaseMetric.setQueryStartTimeNanoseconds(
candidatePhaseMetric.getStartQueryTimeNanoseconds());
mChosenProviderFinalPhaseMetric.setQueryEndTimeNanoseconds(candidatePhaseMetric
.getQueryFinishTimeNanoseconds());
mChosenProviderFinalPhaseMetric.setResponseCollective(
candidatePhaseMetric.getResponseCollective());
mChosenProviderFinalPhaseMetric.setFinalFinishTimeNanoseconds(System.nanoTime());
} catch (Exception e) {
Slog.i(TAG, "Unexpected error during metric candidate to final transfer: " + e);
}
}
/**
* In the final phase, this helps log use cases that were either pure failures or user
* canceled. It's expected that {@link #collectFinalPhaseProviderMetricStatus(boolean,
* ProviderStatusForMetrics) collectFinalPhaseProviderMetricStatus} is called prior to this.
* Otherwise, the logging will miss required bits.
*
* @param isUserCanceledError a boolean indicating if the error was due to user cancelling
*/
public void logFailureOrUserCancel(boolean isUserCanceledError) {
try {
if (isUserCanceledError) {
setHasExceptionFinalPhase(/* has_exception */ false);
logApiCalledAtFinish(
/* apiStatus */ ApiStatus.USER_CANCELED.getMetricCode());
} else {
logApiCalledAtFinish(
/* apiStatus */ ApiStatus.FAILURE.getMetricCode());
}
} catch (Exception e) {
Slog.i(TAG, "Unexpected error during final metric failure emit: " + e);
}
}
/**
* Handles candidate phase metric emit in the RequestSession context, after the candidate phase
* completes.
*
* @param providers a map with known providers and their held metric objects
*/
public void logCandidatePhaseMetrics(Map<String, ProviderSession> providers) {
try {
logApiCalledCandidatePhase(providers, ++mSequenceCounter, mInitialPhaseMetric);
if (mInitialPhaseMetric.getApiName() == GET_CREDENTIAL.getMetricCode()
|| mInitialPhaseMetric.getApiName() == GET_CREDENTIAL_VIA_REGISTRY
.getMetricCode()) {
logApiCalledCandidateGetMetric(providers, mSequenceCounter);
}
} catch (Exception e) {
Slog.i(TAG, "Unexpected error during candidate metric emit: " + e);
}
}
/**
* Handles aggregate candidate phase metric emits in the RequestSession context, after the
* candidate phase completes.
*
* @param providers a map with known providers and their held metric objects
*/
public void logCandidateAggregateMetrics(Map<String, ProviderSession> providers) {
try {
mCandidateAggregateMetric.collectAverages(providers);
logApiCalledAggregateCandidate(mCandidateAggregateMetric, ++mSequenceCounter);
} catch (Exception e) {
Slog.i(TAG, "Unexpected error during aggregate candidate logging " + e);
}
}
/**
* This logs the authentication entry when browsed. Combined with the known browsed clicks
* in the {@link ChosenProviderFinalPhaseMetric}, this fully captures the authentication entry
* logic for multiple loops. An auth entry may have default or missing data, but if a provider
* was never assigned to an auth entry, this indicates an auth entry was never clicked.
* This case is handled in this emit.
*
* @param browsedAuthenticationMetric the authentication metric information to emit
*/
public void logAuthEntry(BrowsedAuthenticationMetric browsedAuthenticationMetric) {
try {
if (browsedAuthenticationMetric.getProviderUid() == DEFAULT_INT_32) {
Slog.v(TAG, "An authentication entry was not clicked");
return;
}
logApiCalledAuthenticationMetric(browsedAuthenticationMetric, ++mSequenceCounter);
} catch (Exception e) {
Slog.i(TAG, "Unexpected error during auth entry metric emit: " + e);
}
}
/**
* Handles the final logging for RequestSession context for the final phase.
*
* @param apiStatus the final status of the api being called
*/
public void logApiCalledAtFinish(int apiStatus) {
try {
logApiCalledFinalPhase(mChosenProviderFinalPhaseMetric, mCandidateBrowsingPhaseMetric,
apiStatus,
++mSequenceCounter);
logApiCalledNoUidFinal(mChosenProviderFinalPhaseMetric, mCandidateBrowsingPhaseMetric,
apiStatus,
++mSequenceCounter);
} catch (Exception e) {
Slog.i(TAG, "Unexpected error during final metric emit: " + e);
}
}
public int getSessionIdTrackTwo() {
return mSessionIdTrackTwo;
}
}