chromium/chrome/browser/password_manager/android/java/src/org/chromium/chrome/browser/password_manager/PasswordSettingsUpdaterMetricsRecorder.java

// Copyright 2022 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

package org.chromium.chrome.browser.password_manager;

import static org.chromium.chrome.browser.password_manager.PasswordManagerSetting.AUTO_SIGN_IN;
import static org.chromium.chrome.browser.password_manager.PasswordManagerSetting.BIOMETRIC_REAUTH_BEFORE_PWD_FILLING;
import static org.chromium.chrome.browser.password_manager.PasswordManagerSetting.OFFER_TO_SAVE_PASSWORDS;

import static java.util.function.Predicate.not;

import android.os.SystemClock;
import android.text.TextUtils;

import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;

import org.chromium.base.metrics.RecordHistogram;

import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;

/**
 * Records metrics for an asynchronous job or a series of jobs. The job is expected to have started
 * when the {@link PasswordSettingsUpdaterMetricsRecorder} instance is created. Latency is reported
 * in {@link #recordMetrics(Exception) recordMetrics} under that assumption.
 */
class PasswordSettingsUpdaterMetricsRecorder {
    private static final String PASSWORD_SETTINGS_HISTOGRAM_BASE =
            "PasswordManager.PasswordSettings";
    static final String SET_VALUE_FUNCTION_SUFFIX = "SetSettingValue";
    static final String GET_VALUE_FUNCTION_SUFFIX = "GetSettingValue";
    public static final String ACCOUNT_STORE_BACKEND_TYPE = "Account";
    public static final String LOCAL_STORE_BACKEND_TYPE = "Local";

    private final @PasswordManagerSetting int mSetting;
    private final String mFunctionSuffix;
    private final long mStartTimeMs;
    private final String mStoreType;

    PasswordSettingsUpdaterMetricsRecorder(
            @PasswordManagerSetting int setting, String functionSuffix, String storeType) {
        mSetting = setting;
        mFunctionSuffix = functionSuffix;
        mStartTimeMs = SystemClock.elapsedRealtime();
        mStoreType = storeType;
    }

    /**
     * Records the metrics depending on {@link Exception} provided.
     * Success metric is always reported. Latency is reported separately for
     * successful and failed operations.
     * Error codes are reported for failed operations only.
     *
     * @param exception {@link Exception} instance corresponding to the occurred error
     */
    void recordMetrics(@Nullable Exception exception) {
        RecordHistogram.recordBooleanHistogram(getHistogramName("Success"), exception == null);
        RecordHistogram.recordBooleanHistogram(
                getHistogramName("Success", mStoreType), exception == null);
        long durationMs = SystemClock.elapsedRealtime() - mStartTimeMs;
        RecordHistogram.recordTimesHistogram(
                getHistogramName(exception == null ? "Latency" : "ErrorLatency"), durationMs);
        RecordHistogram.recordTimesHistogram(
                getHistogramName(exception == null ? "Latency" : "ErrorLatency", mStoreType),
                durationMs);
        if (exception != null) {
            reportErrorMetrics(exception);
        }
    }

    /** Returns histogram suffix (infix in real) for a given setting. */
    private static String getSuffixForSetting(@PasswordManagerSetting int setting) {
        switch (setting) {
            case OFFER_TO_SAVE_PASSWORDS:
                return "OfferToSavePasswords";
            case AUTO_SIGN_IN:
                return "AutoSignIn";
            case BIOMETRIC_REAUTH_BEFORE_PWD_FILLING:
                return "BiometricReauthBeforePwdFilling";
            default:
                assert false : "All settings need to be handled.";
                return "";
        }
    }

    private String getHistogramName(String metric) {
        // Store type will be ignored in the resulting histogram name.
        return getHistogramName(metric, "");
    }

    private String getHistogramName(String metric, String storeType) {
        List<String> histogramNameComponents =
                Arrays.asList(
                        PASSWORD_SETTINGS_HISTOGRAM_BASE,
                        mFunctionSuffix,
                        getSuffixForSetting(mSetting),
                        storeType,
                        metric);
        return histogramNameComponents.stream()
                .filter(not(TextUtils::isEmpty))
                .collect(Collectors.joining("."));
    }

    private void reportErrorMetrics(Exception exception) {
        @AndroidBackendErrorType
        int error = PasswordManagerAndroidBackendUtil.getBackendError(exception);
        RecordHistogram.recordEnumeratedHistogram(
                getHistogramName("ErrorCode"), error, AndroidBackendErrorType.MAX_VALUE + 1);
        RecordHistogram.recordEnumeratedHistogram(
                getHistogramName("ErrorCode", mStoreType),
                error,
                AndroidBackendErrorType.MAX_VALUE + 1);
        if (error == AndroidBackendErrorType.EXTERNAL_ERROR) {
            int apiErrorCode = PasswordManagerAndroidBackendUtil.getApiErrorCode(exception);
            RecordHistogram.recordSparseHistogram(getHistogramName("APIError1"), apiErrorCode);
            RecordHistogram.recordSparseHistogram(
                    getHistogramName("APIError1", mStoreType), apiErrorCode);
        }
    }

    String getFunctionSuffixForTesting() {
        return mFunctionSuffix;
    }

    @VisibleForTesting
    @PasswordManagerSetting
    int getSettingForTesting() {
        return mSetting;
    }

    public static String getStoreType(String account) {
        assert account == null || !account.isEmpty();
        return account == null ? LOCAL_STORE_BACKEND_TYPE : ACCOUNT_STORE_BACKEND_TYPE;
    }
}