blob: 96e0f569f27d18d0491ae882efeaf43be5819dad [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 "components/metrics/structured/structured_metrics_recorder.h"
#include <sstream>
#include <utility>
#include "base/feature_list.h"
#include "base/logging.h"
#include "base/metrics/histogram_macros.h"
#include "base/metrics/metrics_hashes.h"
#include "base/strings/string_number_conversions.h"
#include "base/strings/string_split.h"
#include "base/task/current_thread.h"
#include "base/task/sequenced_task_runner.h"
#include "components/metrics/metrics_features.h"
#include "components/metrics/structured/enums.h"
#include "components/metrics/structured/histogram_util.h"
#include "components/metrics/structured/project_validator.h"
#include "components/metrics/structured/proto/event_storage.pb.h"
#include "components/metrics/structured/structured_metrics_features.h"
#include "components/metrics/structured/structured_metrics_validator.h"
#include "third_party/metrics_proto/chrome_user_metrics_extension.pb.h"
#include "third_party/metrics_proto/structured_data.pb.h"
namespace metrics::structured {
StructuredMetricsRecorder::StructuredMetricsRecorder(
std::unique_ptr<KeyDataProvider> key_data_provider,
std::unique_ptr<EventStorage<StructuredEventProto>> event_storage)
: RefCountedDeleteOnSequence(
base::SequencedTaskRunner::GetCurrentDefault()),
key_data_provider_(std::move(key_data_provider)),
event_storage_(std::move(event_storage)) {
CHECK(key_data_provider_);
CHECK(event_storage_);
Recorder::GetInstance()->SetRecorder(this);
key_data_provider_->AddObserver(this);
}
StructuredMetricsRecorder::~StructuredMetricsRecorder() {
Recorder::GetInstance()->UnsetRecorder(this);
key_data_provider_->RemoveObserver(this);
}
void StructuredMetricsRecorder::EnableRecording() {
DCHECK(base::CurrentUIThread::IsSet());
// Enable recording only if structured metrics' feature flag is enabled.
recording_enabled_ =
base::FeatureList::IsEnabled(features::kStructuredMetrics);
if (recording_enabled_) {
CacheDisallowedProjectsSet();
}
}
void StructuredMetricsRecorder::DisableRecording() {
DCHECK(base::CurrentUIThread::IsSet());
recording_enabled_ = false;
disallowed_projects_.clear();
}
void StructuredMetricsRecorder::ProvideUmaEventMetrics(
ChromeUserMetricsExtension& uma_proto) {
// no-op
}
void StructuredMetricsRecorder::ProvideEventMetrics(
ChromeUserMetricsExtension& uma_proto) {
if (!CanProvideMetrics() || !event_storage_->HasEvents()) {
return;
}
LockStorage();
// Get the events from event storage.
auto events = event_storage_->TakeEvents();
ReleaseStorage();
StructuredDataProto& structured_data = *uma_proto.mutable_structured_data();
*structured_data.mutable_events() = std::move(events);
LogUploadSizeBytes(structured_data.ByteSizeLong());
LogNumEventsInUpload(structured_data.events_size());
}
void StructuredMetricsRecorder::ProvideLogMetadata(
ChromeUserMetricsExtension& uma_proto) {
// Applies custom metadata providers.
Recorder::GetInstance()->OnProvideIndependentMetrics(&uma_proto);
}
bool StructuredMetricsRecorder::CanProvideMetrics() {
// We can provide metrics once device or profile keys have been loaded.
return recording_enabled() && (IsInitialized() || IsProfileInitialized());
}
bool StructuredMetricsRecorder::HasMetricsToProvide() {
return event_storage()->HasEvents();
}
void StructuredMetricsRecorder::OnKeyReady() {
DCHECK(base::CurrentUIThread::IsSet());
// If key data has not been initialized, it is highly likely that the key data
// is initialized.
if (!init_state_.Has(State::kKeyDataInitialized)) {
init_state_.Put(State::kKeyDataInitialized);
} else {
// If kKeyDataInitialized, then this is the second time this callback is
// being called, which must be the profile keys.
init_state_.Put(State::kProfileKeyDataInitialized);
}
// If recorder is now ready then hash events in-memory and store them in
// persistent storage.
if (CanProvideMetrics()) {
HashUnhashedEventsAndPersist();
if (!on_ready_callback_.is_null()) {
std::move(on_ready_callback_).Run();
}
}
}
void StructuredMetricsRecorder::AddEventsObserver(Observer* watcher) {
watchers_.AddObserver(watcher);
}
void StructuredMetricsRecorder::RemoveEventsObserver(Observer* watcher) {
watchers_.RemoveObserver(watcher);
}
void StructuredMetricsRecorder::OnEventRecord(const Event& event) {
DCHECK(base::CurrentUIThread::IsSet());
// One more state for the EventRecordingState exists: kMetricsProviderMissing.
// This is recorded in Recorder::Record.
if (!recording_enabled_ && !CanForceRecord(event)) {
// Events should be ignored if recording is disabled.
LogEventRecordingState(EventRecordingState::kRecordingDisabled);
return;
}
if (IsDeviceEvent(event) && !IsInitialized()) {
RecordEventBeforeInitialization(event);
return;
}
if (IsProfileEvent(event) && !IsProfileInitialized()) {
RecordProfileEventBeforeInitialization(event);
return;
}
RecordEvent(event);
test_callback_on_record_.Run();
}
bool StructuredMetricsRecorder::HasState(State state) const {
return init_state_.Has(state);
}
void StructuredMetricsRecorder::Purge() {
CHECK(event_storage_);
event_storage_->Purge();
key_data_provider_->Purge();
unhashed_events_.clear();
unhashed_profile_events_.clear();
}
void StructuredMetricsRecorder::RecordEventBeforeInitialization(
const Event& event) {
DCHECK(!IsInitialized());
unhashed_events_.emplace_back(event.Clone());
}
void StructuredMetricsRecorder::RecordProfileEventBeforeInitialization(
const Event& event) {
DCHECK(!IsProfileInitialized());
unhashed_profile_events_.emplace_back(event.Clone());
}
void StructuredMetricsRecorder::RecordEvent(const Event& event) {
DCHECK(IsKeyDataInitialized());
// Retrieve key for the project.
KeyData* key_data = key_data_provider_->GetKeyData(event.project_name());
if (!key_data) {
return;
}
// Validates the event. If valid, retrieve the metadata associated
// with the event.
const auto validators = GetEventValidators(event);
if (!validators) {
return;
}
const auto* project_validator = validators->first;
const auto* event_validator = validators->second;
if (!CanUploadProject(project_validator->project_hash())) {
LogEventRecordingState(EventRecordingState::kProjectDisallowed);
return;
}
LogEventRecordingState(EventRecordingState::kRecorded);
// Events associated with UMA are deprecated.
if (project_validator->id_type() == IdType::kUmaId) {
return;
}
StructuredEventProto event_proto;
// Initialize event proto from validator metadata.
InitializeEventProto(&event_proto, event, *project_validator,
*event_validator);
// Sequence-related metadata.
if (project_validator->event_type() == StructuredEventProto::SEQUENCE) {
AddSequenceMetadata(&event_proto, event, *project_validator, *key_data);
}
// Populate the metrics and add to proto.
AddMetricsToProto(&event_proto, event, *project_validator, *event_validator);
// Log size information about the event.
LogEventSerializedSizeBytes(event_proto.ByteSizeLong());
Recorder::GetInstance()->OnEventRecorded(&event_proto);
NotifyEventRecorded(event_proto);
// Add new event to storage.
if (storage_lock_.load()) {
locked_events_.push_back(event_proto);
} else {
event_storage_->AddEvent(event_proto);
}
test_callback_on_record_.Run();
}
void StructuredMetricsRecorder::InitializeEventProto(
StructuredEventProto* proto,
const Event& event,
const ProjectValidator& project_validator,
const EventValidator& event_validator) {
proto->set_project_name_hash(project_validator.project_hash());
proto->set_event_name_hash(event_validator.event_hash());
// Set the event type. Do this with a switch statement to catch when the
// event type is UNKNOWN or uninitialized.
CHECK_NE(project_validator.event_type(), StructuredEventProto::UNKNOWN);
proto->set_event_type(project_validator.event_type());
// Set the ID for this event, if any.
switch (project_validator.id_type()) {
case IdType::kProjectId: {
std::optional<uint64_t> primary_id =
key_data_provider_->GetId(event.project_name());
if (primary_id.has_value()) {
proto->set_profile_event_id(primary_id.value());
}
} break;
case IdType::kUmaId:
// TODO(crbug.com/40156926): Unimplemented.
break;
case IdType::kUnidentified:
// Do nothing.
break;
}
}
void StructuredMetricsRecorder::AddMetricsToProto(
StructuredEventProto* proto,
const Event& event,
const ProjectValidator& project_validator,
const EventValidator& event_validator) {
KeyData* key = key_data_provider_->GetKeyData(event.project_name());
// Key is checked by the calling function.
CHECK(key);
// Set each metric's name hash and value.
for (const auto& metric : event.metric_values()) {
const std::string& metric_name = metric.first;
const Event::MetricValue& metric_value = metric.second;
// Validate that both name and metric type are valid structured metrics.
// If a metric is invalid, then ignore the metric so that other valid
// metrics are added to the proto.
std::optional<EventValidator::MetricMetadata> metadata =
event_validator.GetMetricMetadata(metric_name);
// Checks that the metrics defined are valid. If not valid, then the
// metric will be ignored.
bool is_valid =
metadata.has_value() && metadata->metric_type == metric_value.type;
DCHECK(is_valid);
if (!is_valid) {
continue;
}
StructuredEventProto::Metric* metric_proto = proto->add_metrics();
int64_t metric_name_hash = metadata->metric_name_hash;
metric_proto->set_name_hash(metric_name_hash);
const auto& value = metric_value.value;
switch (metadata->metric_type) {
case Event::MetricType::kHmac:
metric_proto->set_value_hmac(key->HmacMetric(
project_validator.project_hash(), metric_name_hash,
value.GetString(),
base::Days(project_validator.key_rotation_period())));
break;
case Event::MetricType::kLong:
int64_t long_value;
base::StringToInt64(value.GetString(), &long_value);
metric_proto->set_value_int64(long_value);
break;
case Event::MetricType::kRawString:
metric_proto->set_value_string(value.GetString());
break;
case Event::MetricType::kDouble:
metric_proto->set_value_double(value.GetDouble());
break;
// Represents an enum.
case Event::MetricType::kInt:
metric_proto->set_value_int64(value.GetInt());
break;
// Not supported yet.
case Event::MetricType::kBoolean:
break;
}
}
}
void StructuredMetricsRecorder::HashUnhashedEventsAndPersist() {
if (IsInitialized()) {
LogNumEventsRecordedBeforeInit(unhashed_events_.size());
while (!unhashed_events_.empty()) {
OnEventRecord(unhashed_events_.front());
unhashed_events_.pop_front();
}
}
if (IsProfileInitialized()) {
LogNumEventsRecordedBeforeInit(unhashed_profile_events_.size());
while (!unhashed_profile_events_.empty()) {
OnEventRecord(unhashed_profile_events_.front());
unhashed_profile_events_.pop_front();
}
}
}
bool StructuredMetricsRecorder::CanUploadProject(
uint64_t project_name_hash) const {
return !disallowed_projects_.contains(project_name_hash);
}
void StructuredMetricsRecorder::CacheDisallowedProjectsSet() {
const std::string& disallowed_list = GetDisabledProjects();
if (disallowed_list.empty()) {
return;
}
for (const auto& value :
base::SplitString(disallowed_list, ",", base::TRIM_WHITESPACE,
base::SPLIT_WANT_NONEMPTY)) {
uint64_t project_name_hash;
// Parse the string and keep only perfect conversions.
if (base::StringToUint64(value, &project_name_hash)) {
disallowed_projects_.insert(project_name_hash);
}
}
}
bool StructuredMetricsRecorder::IsKeyDataInitialized() {
return key_data_provider_->IsReady();
}
bool StructuredMetricsRecorder::IsInitialized() {
return init_state_.Has(State::kKeyDataInitialized);
}
bool StructuredMetricsRecorder::IsProfileInitialized() {
return init_state_.Has(State::kProfileKeyDataInitialized);
}
bool StructuredMetricsRecorder::CanForceRecord(const Event& event) const {
const auto validators = GetEventValidators(event);
if (!validators) {
return false;
}
return validators->second->can_force_record();
}
bool StructuredMetricsRecorder::IsDeviceEvent(const Event& event) const {
// Validates the event. If valid, retrieve the metadata associated
// with the event.
const auto validators = GetEventValidators(event);
if (!validators) {
return false;
}
const auto* project_validator = validators->first;
// Sequence events are marked as per-device but use the profile keys.
return !event.IsEventSequenceType() &&
project_validator->id_scope() == IdScope::kPerDevice;
}
bool StructuredMetricsRecorder::IsProfileEvent(const Event& event) const {
// Validates the event. If valid, retrieve the metadata associated
// with the event.
const auto validators = GetEventValidators(event);
if (!validators) {
return false;
}
const auto* project_validator = validators->first;
// Sequence events are marked as per-device but use the profile keys.
return event.IsEventSequenceType() ||
project_validator->id_scope() == IdScope::kPerProfile;
}
std::optional<std::pair<const ProjectValidator*, const EventValidator*>>
StructuredMetricsRecorder::GetEventValidators(const Event& event) const {
const auto* project_validator =
validator::Validators::Get()->GetProjectValidator(event.project_name());
if (!project_validator) {
return std::nullopt;
}
const auto* event_validator =
project_validator->GetEventValidator(event.event_name());
if (!event_validator) {
return std::nullopt;
}
return std::make_pair(project_validator, event_validator);
}
void StructuredMetricsRecorder::SetOnReadyToRecord(base::OnceClosure callback) {
on_ready_callback_ = std::move(callback);
if (IsInitialized()) {
std::move(on_ready_callback_).Run();
}
}
void StructuredMetricsRecorder::SetEventRecordCallbackForTest(
base::RepeatingClosure callback) {
test_callback_on_record_ = std::move(callback);
}
void StructuredMetricsRecorder::AddDisallowedProjectForTest(
uint64_t project_name_hash) {
disallowed_projects_.insert(project_name_hash);
}
void StructuredMetricsRecorder::NotifyEventRecorded(
const StructuredEventProto& event) {
for (Observer& watcher : watchers_) {
watcher.OnEventRecorded(event);
}
}
void StructuredMetricsRecorder::LockStorage() {
storage_lock_.store(true);
}
void StructuredMetricsRecorder::ReleaseStorage() {
storage_lock_.store(false);
StoreLockedEvents();
}
void StructuredMetricsRecorder::StoreLockedEvents() {
base::SequencedTaskRunner* task_runner =
Recorder::GetInstance()->GetUiTaskRunner();
if (!task_runner->RunsTasksInCurrentSequence()) {
task_runner->PostTask(
FROM_HERE, base::BindOnce(&StructuredMetricsRecorder::StoreLockedEvents,
weak_factory_.GetWeakPtr()));
return;
}
for (const auto& event : locked_events_) {
event_storage_->AddEvent(event);
}
locked_events_.clear();
}
} // namespace metrics::structured