blob: 91bea8641f9483951a60a5ebafcb74ccce51494a [file] [log] [blame]
// Copyright 2024 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/plus_addresses/metrics/plus_address_submission_logger.h"
#include <memory>
#include <utility>
#include "base/check_deref.h"
#include "base/containers/flat_map.h"
#include "base/metrics/histogram_functions.h"
#include "base/notreached.h"
#include "base/scoped_multi_source_observation.h"
#include "base/strings/utf_string_conversions.h"
#include "base/types/cxx23_to_underlying.h"
#include "components/autofill/core/browser/autofill_field.h"
#include "components/autofill/core/browser/data_model/transliterator.h"
#include "components/autofill/core/browser/form_structure.h"
#include "components/autofill/core/browser/foundations/autofill_client.h"
#include "components/autofill/core/browser/foundations/autofill_manager.h"
#include "components/autofill/core/browser/integrators/plus_addresses/autofill_plus_address_delegate.h"
#include "components/autofill/core/browser/suggestions/suggestion_type.h"
#include "components/autofill/core/common/unique_ids.h"
#include "components/commerce/core/heuristics/commerce_heuristics_provider.h"
#include "components/signin/public/base/consent_level.h"
#include "components/signin/public/identity_manager/account_info.h"
#include "components/signin/public/identity_manager/identity_manager.h"
#include "services/metrics/public/cpp/metrics_utils.h"
#include "services/metrics/public/cpp/ukm_builders.h"
namespace plus_addresses::metrics {
namespace {
using autofill::AutofillField;
using autofill::FieldGlobalId;
using autofill::FormFieldData;
using autofill::FormGlobalId;
using autofill::FormStructure;
using autofill::SuggestionType;
// A bucketed count of plus addresses of the profile.
// These values are persisted to logs. Entries should not be renumbered and
// numeric values should never be reused.
enum class PlusAddressCountBucket {
kNoPlusAddress = 0,
kOneToThreePlusAddresses = 1,
kMoreThanThreePlusAddresses = 2,
kMaxValue = kMoreThanThreePlusAddresses
};
PlusAddressCountBucket ToPlusAddressCountBucket(size_t count) {
if (count == 0) {
return PlusAddressCountBucket::kNoPlusAddress;
} else if (count <= 3) {
return PlusAddressCountBucket::kOneToThreePlusAddresses;
} else {
return PlusAddressCountBucket::kMoreThanThreePlusAddresses;
}
}
bool IsCartOrCheckoutUrl(const GURL& url) {
return commerce_heuristics::IsVisitCheckout(url) ||
commerce_heuristics::IsVisitCart(url);
}
bool IsPlusAddressCreationSuggestion(SuggestionType suggestion_type) {
return suggestion_type == SuggestionType::kCreateNewPlusAddress ||
suggestion_type == SuggestionType::kCreateNewPlusAddressInline;
}
} // namespace
PlusAddressSubmissionLogger::Record::Record(
ukm::SourceId source_id,
bool is_single_field_in_renderer_form,
bool is_first_time_user)
: ukm_builder(source_id),
is_single_field_in_renderer_form(is_single_field_in_renderer_form),
is_first_time_user(is_first_time_user) {}
PlusAddressSubmissionLogger::Record::Record(Record&&) = default;
PlusAddressSubmissionLogger::Record&
PlusAddressSubmissionLogger::Record::operator=(Record&&) = default;
PlusAddressSubmissionLogger::Record::~Record() = default;
PlusAddressSubmissionLogger::PlusAddressSubmissionLogger(
signin::IdentityManager* identity_manager,
PlusAddressVerifier plus_address_verifier)
: identity_manager_(CHECK_DEREF(identity_manager)),
plus_address_verifier_(std::move(plus_address_verifier)) {}
PlusAddressSubmissionLogger::~PlusAddressSubmissionLogger() = default;
void PlusAddressSubmissionLogger::OnPlusAddressSuggestionShown(
autofill::AutofillManager& manager,
FormGlobalId form,
FieldGlobalId field,
autofill::AutofillPlusAddressDelegate::SuggestionContext suggestion_context,
autofill::PasswordFormClassification::Type form_type,
SuggestionType suggestion_type,
size_t plus_address_count) {
const CoreAccountInfo core_account_info =
identity_manager_->GetPrimaryAccountInfo(signin::ConsentLevel::kSignin);
if (core_account_info.IsEmpty()) {
return;
}
// TODO: crbug.com/343124027 - Re-evaluate what to do during paused signin
// status.
const AccountInfo account_info =
identity_manager_->FindExtendedAccountInfo(core_account_info);
FormStructure* form_structure = manager.FindCachedFormById(form);
if (!form_structure) {
return;
}
auto it =
std::ranges::find_if(form_structure->fields(),
[&field](const std::unique_ptr<AutofillField>& f) {
return f->global_id() == field;
});
if (it == form_structure->fields().end()) {
return;
}
FormGlobalId renderer_form_id = (*it)->renderer_form_id();
if (!records_.contains(&manager)) {
managers_observation_.AddObservation(&manager);
}
const size_t field_count_in_renderer_form = std::ranges::count_if(
form_structure->fields(),
[renderer_form_id](
const std::unique_ptr<autofill::AutofillField>& field) {
return field->renderer_form_id() == renderer_form_id;
});
Record record(manager.driver().GetPageUkmSourceId(),
field_count_in_renderer_form == 1,
/*is_first_time_user=*/plus_address_count == 0);
record.ukm_builder
.SetCheckoutOrCartPage(IsCartOrCheckoutUrl(
manager.client().GetLastCommittedPrimaryMainFrameURL()))
.SetFieldCountBrowserForm(ukm::GetExponentialBucketMinForCounts1000(
form_structure->fields().size()))
.SetFieldCountRendererForm(ukm::GetExponentialBucketMinForCounts1000(
field_count_in_renderer_form))
.SetManagedProfile(account_info.IsManaged())
// NewlyCreatedPlusAddress may be reset during submission if no plus
// address was submitted.
.SetNewlyCreatedPlusAddress(
IsPlusAddressCreationSuggestion(suggestion_type))
.SetPasswordFormType(base::to_underlying(form_type))
.SetPlusAddressCount(
base::to_underlying(ToPlusAddressCountBucket(plus_address_count)))
.SetSuggestionContext(base::to_underlying(suggestion_context))
.SetWasShownCreateSuggestion(
IsPlusAddressCreationSuggestion(suggestion_type));
records_[&manager].insert_or_assign(field, std::move(record));
}
void PlusAddressSubmissionLogger::OnAutofillManagerStateChanged(
autofill::AutofillManager& manager,
autofill::AutofillManager::LifecycleState old_state,
autofill::AutofillManager::LifecycleState new_state) {
using enum autofill::AutofillManager::LifecycleState;
switch (new_state) {
case kInactive:
case kActive:
break;
case kPendingReset:
case kPendingDeletion:
RemoveManagerObservation(manager);
break;
}
}
void PlusAddressSubmissionLogger::OnFormSubmitted(
autofill::AutofillManager& manager,
const autofill::FormData& form) {
const CoreAccountInfo core_account_info =
identity_manager_->GetPrimaryAccountInfo(signin::ConsentLevel::kSignin);
if (core_account_info.IsEmpty()) {
return;
}
const AccountInfo account_info =
identity_manager_->FindExtendedAccountInfo(core_account_info);
bool gaia_email_submitted = false;
bool plus_address_submitted = false;
for (const FormFieldData& field : form.fields()) {
// TODO: crbug.com/343124027 - Consider removing whitespace.
const std::string normalized_value = base::UTF16ToUTF8(
autofill::RemoveDiacriticsAndConvertToLowerCase(field.value()));
if (normalized_value == core_account_info.email) {
gaia_email_submitted = true;
} else if (plus_address_verifier_.Run(normalized_value)) {
plus_address_submitted = true;
}
}
if (!gaia_email_submitted && !plus_address_submitted) {
// We could now delete the entries in `records_[&manager]` that correspond
// to fields in this form, but since that happens automatically on every
// page navigation (due to AutofillManager reset/destruction), it is not
// worth the effort.
return;
}
base::flat_map<FieldGlobalId, Record>& records_for_manager =
records_[&manager];
bool has_recorded_submission = false;
for (const FormFieldData& field : form.fields()) {
auto it = records_for_manager.find(field.global_id());
if (it == records_for_manager.end()) {
continue;
}
// Ensure that only a single metric is recorded per form submission. In
// general, there will be multiple fields for which suggestions were shown
// and we pick an arbitrary one.
if (!has_recorded_submission) {
Record& record = it->second;
if (!plus_address_submitted) {
record.ukm_builder.SetNewlyCreatedPlusAddress(false);
}
record.ukm_builder.SetSubmittedPlusAddress(plus_address_submitted);
record.ukm_builder.Record(manager.client().GetUkmRecorder());
has_recorded_submission = true;
// Record a subset of the data also in form of UMAs.
base::UmaHistogramBoolean(kUmaSubmissionPrefix, plus_address_submitted);
base::UmaHistogramBoolean(
base::StrCat({kUmaSubmissionPrefix, ".FirstTimeUser",
account_info.IsManaged() ? ".Yes" : ".No"}),
plus_address_submitted);
base::UmaHistogramBoolean(
base::StrCat({kUmaSubmissionPrefix, ".ManagedUser",
account_info.IsManaged() ? ".Yes" : ".No"}),
plus_address_submitted);
if (record.is_single_field_in_renderer_form) {
base::UmaHistogramBoolean(
base::StrCat({kUmaSubmissionPrefix, ".SingleFieldRendererForm"}),
plus_address_submitted);
}
if (record.is_single_field_in_renderer_form &&
!account_info.IsManaged()) {
base::UmaHistogramBoolean(
base::StrCat({kUmaSubmissionPrefix,
".SingleFieldRendererForm.ManagedUser.No"}),
plus_address_submitted);
}
}
records_for_manager.erase(it);
}
}
void PlusAddressSubmissionLogger::RemoveManagerObservation(
autofill::AutofillManager& manager) {
records_.erase(&manager);
managers_observation_.RemoveObservation(&manager);
}
} // namespace plus_addresses::metrics