// 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 #include #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 key_data_provider, std::unique_ptr 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 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 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> 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