blob: 4496489167b702345f204a12f24505b1bff97e92 [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 "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/storage.pb.h"
#include "components/metrics/structured/structured_metrics_features.h"
#include "components/metrics/structured/structured_metrics_validator.h"
#include "structured_metrics_recorder.h"
#include "third_party/metrics_proto/chrome_user_metrics_extension.pb.h"
namespace metrics::structured {
StructuredMetricsRecorder::StructuredMetricsRecorder(
std::unique_ptr<KeyDataProvider> key_data_provider,
std::unique_ptr<EventStorage> event_storage)
: key_data_provider_(std::move(key_data_provider)),
event_storage_(std::move(event_storage)) {
CHECK(key_data_provider_);
CHECK(event_storage_);
Recorder::GetInstance()->AddObserver(this);
key_data_provider_->AddObserver(this);
}
StructuredMetricsRecorder::~StructuredMetricsRecorder() {
Recorder::GetInstance()->RemoveObserver(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::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);
}
// If kKeyDataInitialized, then this is the second time this callback is being
// called, which must be the profile keys.
else if (init_state_.Has(State::kProfileAdded)) {
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::ProvideUmaEventMetrics(
ChromeUserMetricsExtension& uma_proto) {
// no-op
}
void StructuredMetricsRecorder::ProvideEventMetrics(
ChromeUserMetricsExtension& uma_proto) {
if (!CanProvideMetrics()) {
return;
}
// Get the events from event storage.
event_storage_->MoveEvents(uma_proto);
const auto& structured_data = uma_proto.structured_data();
LogUploadSizeBytes(structured_data.ByteSizeLong());
LogNumEventsInUpload(structured_data.events_size());
// Applies custom metadata providers.
Recorder::GetInstance()->OnProvideIndependentMetrics(&uma_proto);
}
bool StructuredMetricsRecorder::IsInitialized() {
return init_state_.Has(State::kKeyDataInitialized);
}
bool StructuredMetricsRecorder::IsProfileInitialized() {
return init_state_.Has(State::kProfileKeyDataInitialized);
}
void StructuredMetricsRecorder::Purge() {
CHECK(event_storage_);
event_storage_->Purge();
key_data_provider_->Purge();
}
void StructuredMetricsRecorder::OnProfileAdded(
const base::FilePath& profile_path) {
DCHECK(base::CurrentUIThread::IsSet());
// We do not handle multiprofile, instead initializing with the state stored
// in the first logged-in user's cryptohome. So if a second profile is added
// we should ignore it.
if (init_state_.Has(State::kProfileAdded)) {
return;
}
key_data_provider_->OnProfileAdded(profile_path);
init_state_.Put(State::kProfileAdded);
event_storage_->OnProfileAdded(profile_path);
// See DisableRecording for more information.
if (purge_state_on_init_) {
Purge();
purge_state_on_init_ = false;
}
}
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::OnReportingStateChanged(bool enabled) {
DCHECK(base::CurrentUIThread::IsSet());
// When reporting is enabled, OnRecordingEnabled is also called. Let that
// handle enabling.
if (enabled) {
return;
}
// Clean up any events that were recording during the pre-user.
if (!recording_enabled_ && !enabled) {
Purge();
}
// When reporting is disabled, OnRecordingDisabled is also called. Disabling
// here is redundant but done for clarity.
recording_enabled_ = false;
// Delete keys and unsent logs. We need to handle two cases:
//
// 1. A profile hasn't been added yet and we can't delete the files
// immediately. In this case set |purge_state_on_init_| and let
// OnProfileAdded call Purge after initialization.
//
// 2. A profile has been added and so the backing PersistentProtos have been
// constructed. In this case just call Purge directly.
//
// Note that Purge will ensure the events are deleted from disk even if the
// PersistentProto hasn't itself finished being read.
if (!IsInitialized()) {
purge_state_on_init_ = true;
} else {
Purge();
}
}
void StructuredMetricsRecorder::SetOnReadyToRecord(base::OnceClosure callback) {
on_ready_callback_ = std::move(callback);
if (IsInitialized()) {
std::move(on_ready_callback_).Run();
}
}
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 (!IsIndependentMetricsUploadEnabled() ||
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 &&
base::FeatureList::IsEnabled(kEventSequenceLogging)) {
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);
// Add new event to storage.
event_storage_->AddEvent(std::move(event_proto));
}
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: {
absl::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/1148168): 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.
absl::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(), 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;
// Not supported yet.
case Event::MetricType::kInt:
case Event::MetricType::kBoolean:
break;
}
}
}
void StructuredMetricsRecorder::HashUnhashedEventsAndPersist() {
if (IsInitialized()) {
LogNumEventsRecordedBeforeInit(unhashed_events_.size());
while (!unhashed_events_.empty()) {
RecordEvent(unhashed_events_.front());
unhashed_events_.pop_front();
}
}
if (IsProfileInitialized()) {
LogNumEventsRecordedBeforeInit(unhashed_profile_events_.size());
while (!unhashed_profile_events_.empty()) {
RecordEvent(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);
}
}
}
void StructuredMetricsRecorder::AddDisallowedProjectForTest(
uint64_t project_name_hash) {
disallowed_projects_.insert(project_name_hash);
}
bool StructuredMetricsRecorder::IsKeyDataInitialized() {
return key_data_provider_->IsReady();
}
void StructuredMetricsRecorder::SetEventRecordCallbackForTest(
base::RepeatingClosure callback) {
test_callback_on_record_ = std::move(callback);
}
bool StructuredMetricsRecorder::CanForceRecord(const Event& event) const {
const auto validators = GetEventValidators(event);
if (!validators) {
return false;
}
return validators->second->can_force_record();
}
absl::optional<std::pair<const ProjectValidator*, const EventValidator*>>
StructuredMetricsRecorder::GetEventValidators(const Event& event) const {
auto maybe_project_validator =
validator::Validators::Get()->GetProjectValidator(event.project_name());
DCHECK(maybe_project_validator.has_value());
if (!maybe_project_validator.has_value()) {
return {};
}
const auto* project_validator = maybe_project_validator.value();
const auto maybe_event_validator =
project_validator->GetEventValidator(event.event_name());
DCHECK(maybe_event_validator.has_value());
if (!maybe_event_validator.has_value()) {
return {};
}
const auto* event_validator = maybe_event_validator.value();
return std::make_pair(project_validator, event_validator);
}
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;
}
bool StructuredMetricsRecorder::CanProvideMetrics() {
return recording_enabled() && (IsInitialized() || IsProfileInitialized());
}
} // namespace metrics::structured