| // Copyright 2023 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "components/autofill/core/browser/metrics/quality_metrics.h" |
| |
| #include <memory> |
| |
| #include "base/containers/contains.h" |
| #include "base/containers/flat_map.h" |
| #include "base/metrics/histogram_functions.h" |
| #include "components/autofill/core/browser/autofill_data_util.h" |
| #include "components/autofill/core/browser/autofill_field.h" |
| #include "components/autofill/core/browser/field_type_utils.h" |
| #include "components/autofill/core/browser/field_types.h" |
| #include "components/autofill/core/browser/form_structure.h" |
| #include "components/autofill/core/browser/metrics/autofill_metrics_utils.h" |
| #include "components/autofill/core/browser/metrics/field_filling_stats_and_score_metrics.h" |
| #include "components/autofill/core/browser/metrics/granular_filling_metrics_utils.h" |
| #include "components/autofill/core/browser/metrics/placeholder_metrics.h" |
| #include "components/autofill/core/browser/metrics/shadow_prediction_metrics.h" |
| #include "components/autofill/core/browser/validation.h" |
| #include "components/autofill/core/common/autofill_features.h" |
| |
| namespace autofill::autofill_metrics { |
| |
| namespace { |
| |
| void LogNumericQuantityMetrics(const FormStructure& form) { |
| for (const std::unique_ptr<AutofillField>& field : form) { |
| if (field->heuristic_type() != NUMERIC_QUANTITY) { |
| continue; |
| } |
| // For every field that has a heuristics prediction for a |
| // NUMERIC_QUANTITY, log if there was a colliding server |
| // prediction and if the NUMERIC_QUANTITY was a false-positive prediction. |
| // The latter is true when the field was correctly filled. This can |
| // only be recorded when the feature to grant precedence to |
| // NUMERIC_QUANTITY predictions is disabled. |
| bool field_has_non_empty_server_prediction = |
| field->server_type() != UNKNOWN_TYPE && |
| field->server_type() != NO_SERVER_DATA; |
| // Log if there was a colliding server prediction. |
| AutofillMetrics::LogNumericQuantityCollidesWithServerPrediction( |
| field_has_non_empty_server_prediction); |
| // If there was a collision, log if the NUMERIC_QUANTITY was a false |
| // positive since the field was correctly filled. |
| if ((field->is_autofilled() || field->previously_autofilled()) && |
| field_has_non_empty_server_prediction && |
| !base::FeatureList::IsEnabled( |
| features::kAutofillGivePrecedenceToNumericQuantities)) { |
| AutofillMetrics:: |
| LogAcceptedFilledFieldWithNumericQuantityHeuristicPrediction( |
| !field->previously_autofilled()); |
| } |
| } |
| } |
| |
| void LogPerfectFillingMetric(const FormStructure& form) { |
| bool form_has_autofilled_fields = base::ranges::any_of( |
| form, [](const auto& field) { return field->is_autofilled(); }); |
| bool form_has_previously_autofilled_fields = base::ranges::any_of( |
| form, [](const auto& field) { return field->previously_autofilled(); }); |
| // The perfect filling metric is only recorded if Autofill was used on at |
| // least one field. This conditions this metric on Assistance, Readiness and |
| // Acceptance. |
| if (form_has_autofilled_fields || form_has_previously_autofilled_fields) { |
| // A perfectly filled form is submitted as it was filled from Autofill |
| // without subsequent changes. This means that in a perfect filling |
| // scenario, a field is either autofilled, empty, has value at page load or |
| // has value set by JS. |
| bool perfect_filling = base::ranges::none_of(form, [](const auto& field) { |
| return field->is_user_edited() && !field->is_autofilled(); |
| }); |
| // Perfect filling is recorded for addresses and credit cards separately. |
| // Note that a form can be both an address and a credit card form |
| // simultaneously. |
| DenseSet<FormType> form_types = form.GetFormTypes(); |
| if (base::Contains(form_types, FormType::kAddressForm)) { |
| AutofillMetrics::LogAutofillPerfectFilling(/*is_address=*/true, |
| perfect_filling); |
| } |
| if (base::Contains(form_types, FormType::kCreditCardForm)) { |
| AutofillMetrics::LogAutofillPerfectFilling(/*is_address=*/false, |
| perfect_filling); |
| } |
| } |
| } |
| |
| void LogPreFillMetrics(const FormStructure& form) { |
| for (const std::unique_ptr<AutofillField>& field : form) { |
| const FormType form_type_of_field = |
| FieldTypeGroupToFormType(field->Type().group()); |
| const bool is_address_form_field = |
| form_type_of_field == FormType::kAddressForm; |
| const bool credit_card_form_field = |
| form_type_of_field == FormType::kCreditCardForm; |
| if (is_address_form_field || credit_card_form_field) { |
| const std::string_view form_type_name = |
| FormTypeToStringView(form_type_of_field); |
| LogPreFilledFieldStatus(form_type_name, field->initial_value_changed(), |
| field->Type().GetStorableType()); |
| LogPreFilledValueChanged( |
| form_type_name, field->initial_value_changed(), field->value(), |
| field->field_log_events(), field->possible_types(), |
| field->Type().GetStorableType(), field->is_autofilled()); |
| LogPreFilledFieldClassifications(form_type_name, |
| field->initial_value_changed(), |
| field->may_use_prefilled_placeholder()); |
| } |
| } |
| } |
| |
| void LogFieldFillingStatsAndScoreMetrics(const FormStructure& form) { |
| // Tracks how many fields are filled, unfilled or corrected. |
| autofill_metrics::FormGroupFillingStats address_field_stats; |
| autofill_metrics::FormGroupFillingStats cc_field_stats; |
| autofill_metrics::FormGroupFillingStats ac_unrecognized_address_field_stats; |
| // Same as above, but keyed by `FillingMethod`. |
| base::flat_map<FillingMethod, autofill_metrics::FormGroupFillingStats> |
| address_field_stats_by_filling_method; |
| for (const std::unique_ptr<AutofillField>& field : form) { |
| // For any field that belongs to either an address or a credit card form, |
| // collect the type-unspecific field filling statistics. |
| // Those are only emitted when autofill was used on at least one field of |
| // the form. |
| const FormType form_type_of_field = |
| FieldTypeGroupToFormType(field->Type().group()); |
| const bool is_address_form_field = |
| form_type_of_field == FormType::kAddressForm; |
| const bool credit_card_form_field = |
| form_type_of_field == FormType::kCreditCardForm; |
| if (!is_address_form_field && !credit_card_form_field) { |
| continue; |
| } |
| // Address and credit cards fields are mutually exclusive. |
| autofill_metrics::FormGroupFillingStats& group_stats = |
| is_address_form_field ? address_field_stats : cc_field_stats; |
| // Get the filling status of this field and add it to the form group |
| // counter. |
| group_stats.AddFieldFillingStatus( |
| autofill_metrics::GetFieldFillingStatus(*field)); |
| if (is_address_form_field && |
| field->ShouldSuppressSuggestionsAndFillingByDefault()) { |
| ac_unrecognized_address_field_stats.AddFieldFillingStatus( |
| autofill_metrics::GetFieldFillingStatus(*field)); |
| } |
| // For address forms we want to emit filling stats metrics per |
| // `FillingMethod`. Therefore, the stats generated are added to |
| // a map keyed by `FillingMethod`, so that later, metrics can |
| // emitted for each method used. |
| if (base::FeatureList::IsEnabled( |
| features::kAutofillGranularFillingAvailable) & |
| is_address_form_field) { |
| AddFillingStatsForFillingMethod(*field, |
| address_field_stats_by_filling_method); |
| } |
| } |
| // Log the field filling statistics if autofill was used. |
| // The metrics are only emitted if there was at least one field in the |
| // corresponding form group that is or was filled by autofill. |
| // TODO(crbug.com/40274514): Remove this metric on cleanup. |
| autofill_metrics::LogFieldFillingStatsAndScore( |
| address_field_stats, cc_field_stats, ac_unrecognized_address_field_stats); |
| LogAddressFieldFillingStatsAndScoreByFillingMethod( |
| address_field_stats_by_filling_method); |
| } |
| |
| // Logs metrics related to how long it took the user from load/interaction time |
| // till form submission. |
| void LogDurationMetrics(const FormStructure& form, |
| const base::TimeTicks& load_time, |
| const base::TimeTicks& interaction_time, |
| const base::TimeTicks& submission_time) { |
| size_t num_detected_field_types = |
| base::ranges::count_if(form, &FieldHasMeaningfulPossibleFieldTypes, |
| &std::unique_ptr<AutofillField>::operator*); |
| bool form_has_autofilled_fields = base::ranges::any_of( |
| form, [](const auto& field) { return field->is_autofilled(); }); |
| bool has_observed_one_time_code_field = |
| base::ranges::any_of(form, [](const auto& field) { |
| return field->Type().html_type() == HtmlFieldType::kOneTimeCode; |
| }); |
| if (num_detected_field_types >= kMinRequiredFieldsForHeuristics || |
| num_detected_field_types >= kMinRequiredFieldsForQuery) { |
| // `submission_time` should always be available. |
| CHECK(!submission_time.is_null()); |
| // The |load_time| might be unset, in the case that the form was |
| // dynamically added to the DOM. |
| // Submission should chronologically follow form load, however |
| // this might not be true in case of a timezone change. Therefore make |
| // sure to log the elapsed time between submission time and load time only |
| // if it is positive. Same is applied below. |
| if (!load_time.is_null() && submission_time >= load_time) { |
| base::TimeDelta elapsed = submission_time - load_time; |
| if (form_has_autofilled_fields) { |
| AutofillMetrics::LogFormFillDurationFromLoadWithAutofill(elapsed); |
| } else { |
| AutofillMetrics::LogFormFillDurationFromLoadWithoutAutofill(elapsed); |
| } |
| } |
| // The |interaction_time| might be unset, in the case that the user |
| // submitted a blank form. |
| if (!interaction_time.is_null() && submission_time >= interaction_time) { |
| base::TimeDelta elapsed = submission_time - interaction_time; |
| AutofillMetrics::LogFormFillDurationFromInteraction( |
| form.GetFormTypes(), form_has_autofilled_fields, elapsed); |
| } |
| } |
| if (has_observed_one_time_code_field) { |
| if (!load_time.is_null() && submission_time >= load_time) { |
| base::TimeDelta elapsed = submission_time - load_time; |
| AutofillMetrics::LogFormFillDurationFromLoadForOneTimeCode(elapsed); |
| } |
| if (!interaction_time.is_null() && submission_time >= interaction_time) { |
| base::TimeDelta elapsed = submission_time - interaction_time; |
| AutofillMetrics::LogFormFillDurationFromInteractionForOneTimeCode( |
| elapsed); |
| } |
| } |
| } |
| |
| void LogExtractionMetrics(const FormStructure& form) { |
| for (const std::unique_ptr<AutofillField>& field : form) { |
| CHECK(!field->possible_types().empty()); |
| if (FieldHasMeaningfulPossibleFieldTypes(*field)) { |
| base::UmaHistogramEnumeration( |
| "Autofill.LabelInference.InferredLabelSource.AtSubmission2", |
| field->label_source()); |
| } |
| } |
| } |
| |
| void LogPredictionMetrics( |
| const FormStructure& form, |
| AutofillMetrics::FormInteractionsUkmLogger* form_interactions_ukm_logger, |
| bool observed_submission) { |
| const AutofillMetrics::QualityMetricType metric_type = |
| observed_submission ? AutofillMetrics::TYPE_SUBMISSION |
| : AutofillMetrics::TYPE_NO_SUBMISSION; |
| for (const std::unique_ptr<AutofillField>& field : form) { |
| AutofillMetrics::LogHeuristicPredictionQualityMetrics( |
| form_interactions_ukm_logger, form, *field, metric_type); |
| AutofillMetrics::LogServerPredictionQualityMetrics( |
| form_interactions_ukm_logger, form, *field, metric_type); |
| AutofillMetrics::LogOverallPredictionQualityMetrics( |
| form_interactions_ukm_logger, form, *field, metric_type); |
| AutofillMetrics::LogEmailFieldPredictionMetrics(*field); |
| autofill_metrics::LogShadowPredictionComparison(*field); |
| } |
| } |
| |
| void LogFillingMetrics( |
| const FormStructure& form, |
| AutofillMetrics::FormInteractionsUkmLogger* form_interactions_ukm_logger, |
| bool observed_submission) { |
| const AutofillMetrics::QualityMetricType metric_type = |
| observed_submission ? AutofillMetrics::TYPE_SUBMISSION |
| : AutofillMetrics::TYPE_NO_SUBMISSION; |
| for (const std::unique_ptr<AutofillField>& field : form) { |
| form_interactions_ukm_logger->LogFieldFillStatus(form, *field, metric_type); |
| } |
| if (!observed_submission) { |
| return; |
| } |
| LogPerfectFillingMetric(form); |
| LogPreFillMetrics(form); |
| LogFieldFillingStatsAndScoreMetrics(form); |
| |
| FieldTypeSet autofilled_field_types; |
| for (const std::unique_ptr<AutofillField>& field : form) { |
| if (field->is_autofilled() || field->previously_autofilled()) { |
| AutofillMetrics::LogEditedAutofilledFieldAtSubmission( |
| form_interactions_ukm_logger, form, *field); |
| } |
| if (FieldHasMeaningfulPossibleFieldTypes(*field) && |
| field->is_autofilled()) { |
| autofilled_field_types.insert(field->Type().GetStorableType()); |
| } |
| } |
| if (base::Contains(form.GetFormTypes(), FormType::kCreditCardForm)) { |
| AutofillMetrics::LogCreditCardSeamlessnessAtSubmissionTime( |
| autofilled_field_types); |
| } |
| } |
| |
| } // namespace |
| |
| void LogQualityMetrics( |
| const FormStructure& form_structure, |
| const base::TimeTicks& load_time, |
| const base::TimeTicks& interaction_time, |
| const base::TimeTicks& submission_time, |
| AutofillMetrics::FormInteractionsUkmLogger* form_interactions_ukm_logger, |
| bool observed_submission) { |
| // Use the same timestamp on UKM Metrics generated within this method's scope. |
| AutofillMetrics::UkmTimestampPin timestamp_pin(form_interactions_ukm_logger); |
| |
| LogPredictionMetrics(form_structure, form_interactions_ukm_logger, |
| observed_submission); |
| LogFillingMetrics(form_structure, form_interactions_ukm_logger, |
| observed_submission); |
| if (observed_submission) { |
| LogExtractionMetrics(form_structure); |
| LogNumericQuantityMetrics(form_structure); |
| LogDurationMetrics(form_structure, load_time, interaction_time, |
| submission_time); |
| } |
| } |
| |
| // Log the quality of the heuristics and server predictions for this form |
| // structure, if autocomplete attributes are present on the fields (since the |
| // autocomplete attribute takes precedence over other type predictions). |
| void LogQualityMetricsBasedOnAutocomplete( |
| const FormStructure& form_structure, |
| AutofillMetrics::FormInteractionsUkmLogger* form_interactions_ukm_logger) { |
| const AutofillMetrics::QualityMetricType metric_type = |
| AutofillMetrics::TYPE_AUTOCOMPLETE_BASED; |
| for (const auto& field : form_structure) { |
| if (field->html_type() != HtmlFieldType::kUnspecified && |
| field->html_type() != HtmlFieldType::kUnrecognized) { |
| AutofillMetrics::LogHeuristicPredictionQualityMetrics( |
| form_interactions_ukm_logger, form_structure, *field, metric_type); |
| AutofillMetrics::LogServerPredictionQualityMetrics( |
| form_interactions_ukm_logger, form_structure, *field, metric_type); |
| } |
| } |
| } |
| |
| autofill_metrics::FormGroupFillingStats GetFormFillingStatsForFormType( |
| FormType form_type, |
| const FormStructure& form_structure) { |
| autofill_metrics::FormGroupFillingStats filling_stats_for_form_type; |
| |
| for (auto& field : form_structure) { |
| if (FieldTypeGroupToFormType(field->Type().group()) != form_type) { |
| continue; |
| } |
| filling_stats_for_form_type.AddFieldFillingStatus( |
| autofill_metrics::GetFieldFillingStatus(*field)); |
| } |
| return filling_stats_for_form_type; |
| } |
| |
| } // namespace autofill::autofill_metrics |