blob: ba7b168fb210884efa4191d534cca5befe83fc32 [file] [log] [blame]
// 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 "chrome/browser/compose/compose_text_usage_logger.h"
#include <algorithm>
#include <bit>
#include <cstdint>
#include "base/strings/string_tokenizer.h"
#include "base/strings/string_util.h"
#include "components/autofill/content/browser/content_autofill_driver.h"
#include "components/autofill/core/browser/form_structure.h"
#include "components/autofill/core/common/form_field_data.h"
#include "services/metrics/public/cpp/ukm_builders.h"
namespace compose {
DOCUMENT_USER_DATA_KEY_IMPL(ComposeTextUsageLogger);
namespace {
constexpr int MAX_FIELD_METRIC_COUNT = 100;
// Note: Although OnAfterTextFieldDidChange is only called for user
// actions like typing or pastes, we need to handle the case where the
// user edits the field after it was modified by the page. In this case,
// we'd rather be conservative when recording text changes as user typing. If
// the text length changes by more than this number, we'll ignore the change.
constexpr int MAX_CHARS_TYPED_AT_ONCE = 10;
int64_t CountWords(const std::u16string& value) {
int64_t words = 0;
base::String16Tokenizer tokenizer(
value, u"", base::String16Tokenizer::WhitespacePolicy::kSkipOver);
while (tokenizer.GetNext()) {
++words;
}
return words;
}
size_t RoundDownToPowerOfTwo(int64_t n) {
// We use -1 as a special value indicating unknown.
if (n < 0) {
return -1;
}
return std::bit_floor<uint64_t>(n);
}
} // namespace
ComposeTextUsageLogger::FieldMetrics::FieldMetrics() noexcept = default;
ComposeTextUsageLogger::FieldMetrics::~FieldMetrics() = default;
ComposeTextUsageLogger::ComposeTextUsageLogger(content::RenderFrameHost* rfh)
: content::DocumentUserData<ComposeTextUsageLogger>(rfh) {
autofill::ContentAutofillDriver* driver =
autofill::ContentAutofillDriver::GetForRenderFrameHost(rfh);
if (driver) {
driver->GetAutofillManager().AddObserver(this);
}
}
ComposeTextUsageLogger::~ComposeTextUsageLogger() {
Reset();
autofill::ContentAutofillDriver* driver =
autofill::ContentAutofillDriver::GetForRenderFrameHost(
&render_frame_host());
if (driver) {
driver->GetAutofillManager().RemoveObserver(this);
}
}
void ComposeTextUsageLogger::OnAfterTextFieldDidChange(
autofill::AutofillManager& manager,
autofill::FormGlobalId form,
autofill::FieldGlobalId field,
const std::u16string& text_value) {
autofill::FormType form_type = autofill::FormType::kUnknownFormType;
int64_t form_control_type = -1;
autofill::FormStructure* form_structure = manager.FindCachedFormById(form);
if (form_structure) {
const autofill::AutofillField* field_data =
form_structure->GetFieldById(field);
if (field_data) {
form_type = FieldTypeGroupToFormType(field_data->Type().group());
form_control_type = static_cast<int64_t>(field_data->form_control_type);
}
}
// The page UKM source ID should not change while this object is alive. Keep
// a copy of it stored so that we can log safely in the destructor.
DCHECK(source_id_ == ukm::SourceId() ||
source_id_ == render_frame_host().GetPageUkmSourceId())
<< "source_id shouldn't change";
if (source_id_ == ukm::SourceId()) {
source_id_ = render_frame_host().GetPageUkmSourceId();
}
if (field_metrics_.size() >= MAX_FIELD_METRIC_COUNT) {
Reset();
}
FieldMetrics& metrics = field_metrics_[field];
if (!metrics.initialized) {
if (text_value.length() > MAX_CHARS_TYPED_AT_ONCE) {
metrics.initial_text = text_value;
}
metrics.initialized = true;
}
switch (form_type) {
case autofill::FormType::kUnknownFormType:
break;
case autofill::FormType::kAddressForm:
metrics.is_autofill_field_type = true;
break;
case autofill::FormType::kCreditCardForm:
case autofill::FormType::kPasswordForm:
metrics.sensitive_field = true;
metrics.is_autofill_field_type = true;
break;
}
// Note that field_data->value doesn't have the current value, so we use
// text_value instead.
const int64_t new_length = text_value.size();
const int64_t delta =
new_length - static_cast<int64_t>(metrics.initial_text.size());
if (delta > 0 && delta <= MAX_CHARS_TYPED_AT_ONCE) {
metrics.estimate_typed_characters += delta;
}
metrics.form_control_type = form_control_type;
metrics.final_text = std::move(text_value);
}
void ComposeTextUsageLogger::Reset() {
if (field_metrics_.empty()) {
return;
}
for (const auto& entry : field_metrics_) {
const FieldMetrics& metrics = entry.second;
if (metrics.final_text.size() == 0) {
continue;
}
int64_t typed_chars = -1;
int64_t typed_words = -1;
if (!metrics.sensitive_field) {
int64_t size_change = static_cast<int64_t>(metrics.final_text.size()) -
static_cast<int64_t>(metrics.initial_text.size());
typed_chars = std::min(metrics.estimate_typed_characters, size_change);
typed_words = std::max<int64_t>(
0, CountWords(metrics.final_text) - CountWords(metrics.initial_text));
}
ukm::builders::Compose_TextElementUsage(source_id_)
.SetAutofillFormControlType(metrics.form_control_type)
.SetTypedCharacterCount(RoundDownToPowerOfTwo(typed_chars))
.SetTypedWordCount(RoundDownToPowerOfTwo(typed_words))
.SetIsAutofillFieldType(metrics.is_autofill_field_type)
.Record(ukm::UkmRecorder::Get());
}
field_metrics_.clear();
}
} // namespace compose