From cbe59d4cd222ca7dd0f221af9f7d24a7d8d1c7fd Mon Sep 17 00:00:00 2001 From: Darnell Andries Date: Tue, 14 Apr 2026 15:47:18 -0700 Subject: [PATCH] Add custom attribute support for metrics (#35418) * Add custom attribute support for metrics * Rename `EventReceiver` to `EventRelay` * Address PR feedback --- components/p3a/BUILD.gn | 6 +- components/p3a/metric_config.cc | 25 ++++ components/p3a/metric_config.h | 11 +- components/p3a/metric_config_unittest.cc | 51 ++++++++ components/p3a/p3a_message.cc | 38 ++++++ components/p3a/p3a_message.h | 3 + components/p3a/p3a_message_unittest.cc | 146 ++++++++++++++++++++++ components/p3a/p3a_service.cc | 15 +++ components/p3a/p3a_service.h | 11 ++ components/p3a/p3a_service_unittest.cc | 16 +++ components/p3a/pref_names.h | 1 + components/p3a/remote_config_manager.cc | 27 +++- components/p3a/remote_config_manager.h | 1 + components/p3a_utils/BUILD.gn | 4 + components/p3a_utils/custom_attributes.cc | 18 +++ components/p3a_utils/custom_attributes.h | 23 ++++ components/p3a_utils/event_relay.cc | 34 +++++ components/p3a_utils/event_relay.h | 50 ++++++++ 18 files changed, 476 insertions(+), 4 deletions(-) create mode 100644 components/p3a/metric_config_unittest.cc create mode 100644 components/p3a/p3a_message_unittest.cc create mode 100644 components/p3a_utils/custom_attributes.cc create mode 100644 components/p3a_utils/custom_attributes.h create mode 100644 components/p3a_utils/event_relay.cc create mode 100644 components/p3a_utils/event_relay.h diff --git a/components/p3a/BUILD.gn b/components/p3a/BUILD.gn index 708cafdb3f7..596ab46ab07 100644 --- a/components/p3a/BUILD.gn +++ b/components/p3a/BUILD.gn @@ -60,6 +60,8 @@ static_library("p3a") { "utils.h", ] + public_deps = [ "//brave/components/p3a_utils" ] + deps = [ "constellation/rs/cxx:rust_lib", "//base", @@ -68,7 +70,6 @@ static_library("p3a") { "//brave/components/brave_origin/buildflags", "//brave/components/brave_stats/browser", "//brave/components/l10n/common", - "//brave/components/p3a_utils", "//brave/components/version_info", "//brave/vendor/brave_base", "//components/cbor", @@ -99,10 +100,12 @@ source_set("unit_tests") { "constellation_helper_unittest.cc", "constellation_log_store_unittest.cc", "message_manager_unittest.cc", + "metric_config_unittest.cc", "metric_log_store_unittest.cc", "metric_names_unittest.cc", "nitro_utils/attestation_unittest.cc", "nitro_utils/cose_unittest.cc", + "p3a_message_unittest.cc", "p3a_service_unittest.cc", "region_unittest.cc", "remote_config_manager_unittest.cc", @@ -117,6 +120,7 @@ source_set("unit_tests") { "//brave/components/brave_stats/browser", "//brave/components/constants", "//brave/components/p3a", + "//brave/components/p3a_utils", "//components/cbor", "//components/prefs:test_support", "//net", diff --git a/components/p3a/metric_config.cc b/components/p3a/metric_config.cc index d8094de8507..979a791ed8a 100644 --- a/components/p3a/metric_config.cc +++ b/components/p3a/metric_config.cc @@ -30,6 +30,7 @@ constexpr auto kMetricAttributeMap = {"dtoa", MetricAttribute::kDateOfActivation}, {"woa", MetricAttribute::kWeekOfActivation}, {"is_browser_default", MetricAttribute::kIsBrowserDefault}, + {"custom_attribute", MetricAttribute::kCustomAttribute}, }); bool GetMetricAttribute(const base::Value* value, @@ -120,6 +121,27 @@ bool GetOptionalBool(const base::Value* value, std::optional* field) { return true; } +bool GetCustomAttributes(const base::Value* value, + std::optional* field) { + if (!value || !value->is_list()) { + return false; + } + + const auto& list = value->GetList(); + + RemoteCustomAttributes attributes; + + for (size_t i = 0; i < list.size() && i < attributes.size(); i++) { + if (!list[i].is_string()) { + return false; + } + attributes[i] = list[i].GetString(); + } + + *field = attributes; + return true; +} + } // namespace RemoteMetricConfig::RemoteMetricConfig() = default; @@ -151,6 +173,9 @@ void RemoteMetricConfig::RegisterJSONConverter( &GetOptionalString); converter->RegisterCustomValueField("cadence", &RemoteMetricConfig::cadence, &GetMetricLogType); + converter->RegisterCustomValueField("custom_attributes", + &RemoteMetricConfig::custom_attributes, + &GetCustomAttributes); } } // namespace p3a diff --git a/components/p3a/metric_config.h b/components/p3a/metric_config.h index 73b89625d66..7dc68ff1008 100644 --- a/components/p3a/metric_config.h +++ b/components/p3a/metric_config.h @@ -35,7 +35,8 @@ enum class MetricAttribute { kWeekOfActivation, kDateOfActivation, kIsBrowserDefault, - kMaxValue = kIsBrowserDefault, + kCustomAttribute, + kMaxValue = kCustomAttribute, }; inline constexpr MetricAttribute kDefaultMetricAttributes[] = { @@ -47,6 +48,8 @@ inline constexpr MetricAttribute kDefaultMetricAttributes[] = { using MetricAttributes = std::array, 8>; using MetricAttributesToAppend = std::array, 2>; +using CustomAttributes = std::array, 2>; +using RemoteCustomAttributes = std::array, 2>; struct MetricConfig { // Once the metric value has been sent, the value will be removed from the log @@ -72,6 +75,11 @@ struct MetricConfig { // If specified in a remote configuration, the cadence of the metric will be // overridden. std::optional cadence; + + // Custom attribute key names to include with the metric. Each + // kCustomAttribute in the attributes list is replaced in order with the next + // key from this array. + CustomAttributes custom_attributes; }; // This struct is used to store the remote configuration for a metric. @@ -91,6 +99,7 @@ struct RemoteMetricConfig { std::optional record_activation_date; std::optional activation_metric_name; std::optional cadence; + std::optional custom_attributes; static void RegisterJSONConverter( base::JSONValueConverter* converter); diff --git a/components/p3a/metric_config_unittest.cc b/components/p3a/metric_config_unittest.cc new file mode 100644 index 00000000000..2d7a6ccdf2c --- /dev/null +++ b/components/p3a/metric_config_unittest.cc @@ -0,0 +1,51 @@ +/* Copyright (c) 2026 The Brave Authors. All rights reserved. + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at https://mozilla.org/MPL/2.0/. */ + +#include "brave/components/p3a/metric_config.h" + +#include "base/json/json_value_converter.h" +#include "base/values.h" +#include "testing/gtest/include/gtest/gtest.h" + +namespace p3a { + +namespace { + +RemoteMetricConfig ConvertFromDict(base::DictValue dict) { + base::JSONValueConverter converter; + RemoteMetricConfig config; + converter.Convert(base::Value(std::move(dict)), &config); + return config; +} + +} // namespace + +TEST(P3AMetricConfigTest, CustomAttributesValidList) { + auto dict = base::DictValue().Set( + "custom_attributes", base::ListValue().Append("attr_a").Append("attr_b")); + auto config = ConvertFromDict(std::move(dict)); + ASSERT_TRUE(config.custom_attributes.has_value()); + EXPECT_EQ((*config.custom_attributes)[0], "attr_a"); + EXPECT_EQ((*config.custom_attributes)[1], "attr_b"); +} + +TEST(P3AMetricConfigTest, CustomAttributesNonList) { + // custom_attributes must be a list; a non-list value should leave the field + // unset. + auto dict = base::DictValue().Set("custom_attributes", "not_a_list"); + auto config = ConvertFromDict(std::move(dict)); + EXPECT_FALSE(config.custom_attributes.has_value()); +} + +TEST(P3AMetricConfigTest, CustomAttributesListWithNonStringElement) { + // A list element that is not a string should cause parsing to fail and leave + // the field unset. + auto dict = base::DictValue().Set( + "custom_attributes", base::ListValue().Append("valid_attr").Append(42)); + auto config = ConvertFromDict(std::move(dict)); + EXPECT_FALSE(config.custom_attributes.has_value()); +} + +} // namespace p3a diff --git a/components/p3a/p3a_message.cc b/components/p3a/p3a_message.cc index b1ba1803756..872acbaa7d0 100644 --- a/components/p3a/p3a_message.cc +++ b/components/p3a/p3a_message.cc @@ -56,6 +56,7 @@ constexpr char kSubregionAttributeName[] = "subregion"; constexpr char kRefAttributeName[] = "ref"; constexpr char kIsBrowserDefaultAttributeName[] = "is_default"; +constexpr char kCustomAttributeKeyPrefix[] = "custom_"; constexpr char kOrganicRefPrefix[] = "BRV"; constexpr char kNone[] = "none"; constexpr char kRefOther[] = "other"; @@ -105,6 +106,26 @@ std::string InferActivationDate(const MessageMetainfo& meta, return FormatUTCDateFromTime(*activation_date); } +std::optional> FetchCustomAttribute( + const MessageMetainfo& meta, + const MetricConfig* metric_config, + size_t custom_attr_index) { + if (!metric_config || + custom_attr_index >= metric_config->custom_attributes.size()) { + return std::nullopt; + } + const auto& key_opt = metric_config->custom_attributes[custom_attr_index]; + if (!key_opt) { + return std::nullopt; + } + auto value = meta.GetCustomAttribute(*key_opt); + if (!value) { + return std::nullopt; + } + return std::array{ + base::StrCat({kCustomAttributeKeyPrefix, *key_opt}), std::move(*value)}; +} + std::vector> PopulateConstellationAttributes( const std::string_view metric_name, const uint64_t metric_value, @@ -126,6 +147,7 @@ std::vector> PopulateConstellationAttributes( attributes = {{kMetricNameAttributeName, std::string(metric_name)}}; } std::string attribute_value; + size_t custom_attr_index = 0; for (const auto& attribute : attributes_to_load) { switch (attribute) { @@ -214,6 +236,12 @@ std::vector> PopulateConstellationAttributes( {kIsBrowserDefaultAttributeName, base::ToString(meta.is_browser_default().value_or(false))}); break; + case MetricAttribute::kCustomAttribute: + if (auto attr = FetchCustomAttribute(meta, metric_config, + custom_attr_index++)) { + attributes.push_back(std::move(*attr)); + } + break; } } return attributes; @@ -390,4 +418,14 @@ std::optional MessageMetainfo::GetActivationDate( return base::ValueToTime(*time_val); } +std::optional MessageMetainfo::GetCustomAttribute( + std::string_view attribute_name) const { + const auto* value = local_state_->GetDict(kCustomAttributesDictPref) + .FindString(attribute_name); + if (!value || value->empty()) { + return std::nullopt; + } + return *value; +} + } // namespace p3a diff --git a/components/p3a/p3a_message.h b/components/p3a/p3a_message.h index ee92f4c3f65..4f9ed3407c5 100644 --- a/components/p3a/p3a_message.h +++ b/components/p3a/p3a_message.h @@ -43,6 +43,9 @@ class MessageMetainfo { std::optional GetActivationDate( std::string_view histogram_name) const; + std::optional GetCustomAttribute( + std::string_view attribute_name) const; + const std::string& platform() const { return platform_; } const std::string& general_platform() const { return general_platform_; } const std::string& channel() const { return channel_; } diff --git a/components/p3a/p3a_message_unittest.cc b/components/p3a/p3a_message_unittest.cc new file mode 100644 index 00000000000..9193dd2d15d --- /dev/null +++ b/components/p3a/p3a_message_unittest.cc @@ -0,0 +1,146 @@ +// Copyright (c) 2026 The Brave Authors. All rights reserved. +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this file, +// You can obtain one at https://mozilla.org/MPL/2.0/. + +#include "brave/components/p3a/p3a_message.h" + +#include "base/time/time.h" +#include "brave/components/p3a/metric_config.h" +#include "brave/components/p3a/pref_names.h" +#include "brave/components/p3a/uploader.h" +#include "components/prefs/pref_registry_simple.h" +#include "components/prefs/scoped_user_pref_update.h" +#include "components/prefs/testing_pref_service.h" +#include "testing/gtest/include/gtest/gtest.h" + +#if !BUILDFLAG(IS_IOS) +#include "brave/components/brave_referrals/common/pref_names.h" +#endif // !BUILDFLAG(IS_IOS) + +namespace p3a { + +namespace { + +constexpr char kTestMetricName[] = "Brave.Test.Message"; +constexpr uint64_t kTestMetricValue = 3; + +} // namespace + +class P3AMessageTest : public testing::Test { + protected: + void SetUp() override { + local_state_.registry()->RegisterDictionaryPref(kActivationDatesDictPref); + local_state_.registry()->RegisterDictionaryPref(kCustomAttributesDictPref); + +#if !BUILDFLAG(IS_IOS) + local_state_.registry()->RegisterStringPref(kReferralPromoCode, {}); +#endif + + base::Time install_time; + ASSERT_TRUE(base::Time::FromString("2020-01-01", &install_time)); + meta_.Init(&local_state_, "release", install_time); + } + + TestingPrefServiceSimple local_state_; + MessageMetainfo meta_; +}; + +// Verifies GenerateP3AConstellationMessage serializes metric name, value, and +// channel correctly using fully deterministic attributes. +TEST_F(P3AMessageTest, Basic) { + constexpr MetricConfig kConfig = { + .attributes = MetricAttributes{MetricAttribute::kAnswerIndex, + MetricAttribute::kChannel, + MetricAttribute::kDateOfInstall}, + }; + + std::string message = GenerateP3AConstellationMessage( + kTestMetricName, kTestMetricValue, meta_, kP3AUploadType, &kConfig); + + EXPECT_EQ(message, + "metric_name|Brave.Test.Message" + ";metric_value|3" + ";channel|release" + ";dtoi|none"); + + constexpr MetricConfig kNullAttrConfig = { + .attributes = MetricAttributes{MetricAttribute::kAnswerIndex, + std::nullopt, MetricAttribute::kChannel, + MetricAttribute::kDateOfInstall}, + }; + + std::string message_null_attr = + GenerateP3AConstellationMessage(kTestMetricName, kTestMetricValue, meta_, + kP3AUploadType, &kNullAttrConfig); + + EXPECT_EQ(message_null_attr, + "metric_name|Brave.Test.Message" + ";metric_value|3" + ";channel|release" + ";dtoi|none"); +} + +// Verifies that custom attributes are included with the "custom_" prefix, and +// that missing attributes are not included in the message. +TEST_F(P3AMessageTest, CustomAttributes) { + { + ScopedDictPrefUpdate update(&local_state_, kCustomAttributesDictPref); + update->Set("color", "blue"); + update->Set("size", "large"); + } + + constexpr MetricConfig kConfig = { + .attributes = MetricAttributes{MetricAttribute::kAnswerIndex, + MetricAttribute::kCustomAttribute, + MetricAttribute::kCustomAttribute, + MetricAttribute::kDateOfInstall}, + .custom_attributes = CustomAttributes{"color", "size"}, + }; + + std::string message = GenerateP3AConstellationMessage( + kTestMetricName, kTestMetricValue, meta_, kP3AUploadType, &kConfig); + + EXPECT_EQ(message, + "metric_name|Brave.Test.Message" + ";metric_value|3" + ";custom_color|blue" + ";custom_size|large" + ";dtoi|none"); + + constexpr MetricConfig kMissingFirstConfig = { + .attributes = MetricAttributes{MetricAttribute::kAnswerIndex, + MetricAttribute::kCustomAttribute, + MetricAttribute::kCustomAttribute, + MetricAttribute::kDateOfInstall}, + .custom_attributes = CustomAttributes{"nonexistent", "color"}, + }; + + std::string message_missing_first = + GenerateP3AConstellationMessage(kTestMetricName, kTestMetricValue, meta_, + kP3AUploadType, &kMissingFirstConfig); + + EXPECT_EQ(message_missing_first, + "metric_name|Brave.Test.Message" + ";metric_value|3" + ";custom_color|blue" + ";dtoi|none"); + + constexpr MetricConfig kAllMissingConfig = { + .attributes = MetricAttributes{MetricAttribute::kAnswerIndex, + MetricAttribute::kCustomAttribute, + MetricAttribute::kDateOfInstall}, + .custom_attributes = CustomAttributes{"nonexistent"}, + }; + + std::string message_all_missing = + GenerateP3AConstellationMessage(kTestMetricName, kTestMetricValue, meta_, + kP3AUploadType, &kAllMissingConfig); + + EXPECT_EQ(message_all_missing, + "metric_name|Brave.Test.Message" + ";metric_value|3" + ";dtoi|none"); +} + +} // namespace p3a diff --git a/components/p3a/p3a_service.cc b/components/p3a/p3a_service.cc index 609b29655e4..7f2e7817e55 100644 --- a/components/p3a/p3a_service.cc +++ b/components/p3a/p3a_service.cc @@ -22,6 +22,7 @@ #include "brave/components/p3a/metric_names.h" #include "brave/components/p3a/p3a_config.h" #include "brave/components/p3a/pref_names.h" +#include "brave/components/p3a_utils/event_relay.h" #include "components/prefs/pref_registry_simple.h" #include "components/prefs/pref_service.h" #include "components/prefs/scoped_user_pref_update.h" @@ -84,6 +85,8 @@ P3AService::P3AService(PrefService& local_state, message_manager_ = std::make_unique( local_state, &config_, *this, channel, first_run_time); + + event_relay_observation_.Observe(p3a_utils::EventRelay::GetInstance()); } P3AService::~P3AService() = default; @@ -95,6 +98,7 @@ void P3AService::RegisterPrefs(PrefRegistrySimple* registry, bool first_run) { // New users are shown the P3A notice via the welcome page. registry->RegisterBooleanPref(kP3ANoticeAcknowledged, first_run); + registry->RegisterDictionaryPref(kCustomAttributesDictPref); registry->RegisterDictionaryPref(kDynamicMetricsDictPref); registry->RegisterDictionaryPref(kActivationDatesDictPref); } @@ -342,6 +346,17 @@ void P3AService::OnDefaultBrowserStatusChanged(bool is_default) { } #endif // !BUILDFLAG(IS_IOS) +void P3AService::OnCustomAttributeSet( + std::string_view attribute_name, + std::optional attribute_value) { + ScopedDictPrefUpdate update(&*local_state_, kCustomAttributesDictPref); + if (attribute_value) { + update->Set(attribute_name, *attribute_value); + } else { + update->Remove(attribute_name); + } +} + void P3AService::HandleHistogramChange(std::string_view histogram_name, size_t bucket) { VLOG(2) << "P3AService::OnHistogramChanged: histogram_name = " diff --git a/components/p3a/p3a_service.h b/components/p3a/p3a_service.h index 97614ac5276..ac08ffc7400 100644 --- a/components/p3a/p3a_service.h +++ b/components/p3a/p3a_service.h @@ -24,6 +24,7 @@ #include "brave/components/p3a/metric_log_type.h" #include "brave/components/p3a/p3a_config.h" #include "brave/components/p3a/remote_config_manager.h" +#include "brave/components/p3a_utils/event_relay.h" #include "build/build_config.h" #include "components/prefs/pref_change_registrar.h" @@ -57,6 +58,7 @@ class P3AService : public base::RefCountedThreadSafe, #if !BUILDFLAG(IS_IOS) public misc_metrics::DefaultBrowserMonitor::Observer, #endif // !BUILDFLAG(IS_IOS) + public p3a_utils::EventRelay::Observer, public RemoteConfigManager::Delegate { public: P3AService(PrefService& local_state, @@ -128,6 +130,11 @@ class P3AService : public base::RefCountedThreadSafe, // RemoteConfigManager::Delegate void OnRemoteConfigLoaded() override; + // p3a_utils::EventRelay::Observer + void OnCustomAttributeSet( + std::string_view attribute_name, + std::optional attribute_value) override; + #if !BUILDFLAG(IS_IOS) // misc_metrics::DefaultBrowserMonitor::Observer void OnDefaultBrowserStatusChanged(bool is_default) override; @@ -191,6 +198,10 @@ class P3AService : public base::RefCountedThreadSafe, base::RepeatingCallbackList metric_cycled_callbacks_; + base::ScopedObservation + event_relay_observation_{this}; + #if !BUILDFLAG(IS_IOS) base::ScopedObservation diff --git a/components/p3a/p3a_service_unittest.cc b/components/p3a/p3a_service_unittest.cc index bd6dab9514e..8ee04c1c09e 100644 --- a/components/p3a/p3a_service_unittest.cc +++ b/components/p3a/p3a_service_unittest.cc @@ -11,6 +11,7 @@ #include "base/test/values_test_util.h" #include "base/time/time.h" #include "brave/components/p3a/pref_names.h" +#include "brave/components/p3a_utils/custom_attributes.h" #include "components/prefs/testing_pref_service.h" #include "content/public/test/browser_task_environment.h" #include "services/network/public/cpp/weak_wrapper_shared_url_loader_factory.h" @@ -139,4 +140,19 @@ TEST_F(P3AServiceTest, MetricValueStored) { EXPECT_TRUE(stored_log != nullptr); } +TEST_F(P3AServiceTest, CustomAttributeStored) { + CreateP3AService(); + + p3a_utils::SetCustomAttribute("foo", "bar"); + + const auto* value = + local_state_.GetDict(kCustomAttributesDictPref).FindString("foo"); + ASSERT_TRUE(value != nullptr); + EXPECT_EQ(*value, "bar"); + + p3a_utils::SetCustomAttribute("foo", std::nullopt); + EXPECT_FALSE( + local_state_.GetDict(kCustomAttributesDictPref).FindString("foo")); +} + } // namespace p3a diff --git a/components/p3a/pref_names.h b/components/p3a/pref_names.h index 354a721804d..9eb1353c563 100644 --- a/components/p3a/pref_names.h +++ b/components/p3a/pref_names.h @@ -40,6 +40,7 @@ inline constexpr char kLastExpressConstellationRotationTimeStampPref[] = "p3a.last_express_constellation_rotation_timestamp"; // P3A service preference names +inline constexpr char kCustomAttributesDictPref[] = "p3a.custom_attributes"; inline constexpr char kDynamicMetricsDictPref[] = "p3a.dynamic_metrics"; // Star randomness meta preference names diff --git a/components/p3a/remote_config_manager.cc b/components/p3a/remote_config_manager.cc index 302a69fe926..c3302976d61 100644 --- a/components/p3a/remote_config_manager.cc +++ b/components/p3a/remote_config_manager.cc @@ -107,13 +107,24 @@ void RemoteConfigManager::SetMetricConfigs( metric_configs_.clear(); activation_metric_names_.clear(); + custom_attribute_names_.clear(); for (const auto& entry : *result) { - if (!entry.second.activation_metric_name || + if ((!entry.second.activation_metric_name && + !entry.second.custom_attributes) || !delegate_->GetLogTypeForHistogram(entry.first)) { continue; } - activation_metric_names_.insert(*entry.second.activation_metric_name); + if (entry.second.activation_metric_name) { + activation_metric_names_.insert(*entry.second.activation_metric_name); + } + if (entry.second.custom_attributes) { + for (const auto& attr : *entry.second.custom_attributes) { + if (attr) { + custom_attribute_names_.insert(*attr); + } + } + } } for (const auto& entry : *result) { @@ -151,6 +162,18 @@ void RemoteConfigManager::SetMetricConfigs( if (remote_config.cadence) { metric_config.cadence = remote_config.cadence; } + if (remote_config.custom_attributes) { + for (size_t i = 0; i < remote_config.custom_attributes->size(); i++) { + const auto& attr = (*remote_config.custom_attributes)[i]; + if (!attr) { + continue; + } + auto it = custom_attribute_names_.find(*attr); + if (it != custom_attribute_names_.end()) { + metric_config.custom_attributes[i] = *it; + } + } + } metric_configs_.emplace(entry.first, metric_config); } diff --git a/components/p3a/remote_config_manager.h b/components/p3a/remote_config_manager.h index 7f76e1cc467..96f42ded4da 100644 --- a/components/p3a/remote_config_manager.h +++ b/components/p3a/remote_config_manager.h @@ -57,6 +57,7 @@ class RemoteConfigManager { base::flat_map metric_configs_; base::flat_set activation_metric_names_; + base::flat_set custom_attribute_names_; bool is_loaded_ = false; diff --git a/components/p3a_utils/BUILD.gn b/components/p3a_utils/BUILD.gn index 802c74ad068..9f3206321c6 100644 --- a/components/p3a_utils/BUILD.gn +++ b/components/p3a_utils/BUILD.gn @@ -1,6 +1,10 @@ static_library("p3a_utils") { sources = [ "bucket.h", + "custom_attributes.cc", + "custom_attributes.h", + "event_relay.cc", + "event_relay.h", "feature_usage.cc", "feature_usage.h", ] diff --git a/components/p3a_utils/custom_attributes.cc b/components/p3a_utils/custom_attributes.cc new file mode 100644 index 00000000000..2c30e2daae0 --- /dev/null +++ b/components/p3a_utils/custom_attributes.cc @@ -0,0 +1,18 @@ +/* Copyright (c) 2026 The Brave Authors. All rights reserved. + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at https://mozilla.org/MPL/2.0/. */ + +#include "brave/components/p3a_utils/custom_attributes.h" + +#include "brave/components/p3a_utils/event_relay.h" + +namespace p3a_utils { + +void SetCustomAttribute(std::string_view attribute_name, + std::optional attribute_value) { + EventRelay::GetInstance()->NotifyCustomAttributeSet(attribute_name, + attribute_value); +} + +} // namespace p3a_utils diff --git a/components/p3a_utils/custom_attributes.h b/components/p3a_utils/custom_attributes.h new file mode 100644 index 00000000000..b86c949a661 --- /dev/null +++ b/components/p3a_utils/custom_attributes.h @@ -0,0 +1,23 @@ +/* Copyright (c) 2026 The Brave Authors. All rights reserved. + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at https://mozilla.org/MPL/2.0/. */ + +#ifndef BRAVE_COMPONENTS_P3A_UTILS_CUSTOM_ATTRIBUTES_H_ +#define BRAVE_COMPONENTS_P3A_UTILS_CUSTOM_ATTRIBUTES_H_ + +#include +#include + +namespace p3a_utils { + +// Sets a custom attribute value, which can be included +// in any metric report, depending on metric configuration. +// If nullopt is provided for attribute_value, the attribute +// will be cleared. +void SetCustomAttribute(std::string_view attribute_name, + std::optional attribute_value); + +} // namespace p3a_utils + +#endif // BRAVE_COMPONENTS_P3A_UTILS_CUSTOM_ATTRIBUTES_H_ diff --git a/components/p3a_utils/event_relay.cc b/components/p3a_utils/event_relay.cc new file mode 100644 index 00000000000..002f6193c98 --- /dev/null +++ b/components/p3a_utils/event_relay.cc @@ -0,0 +1,34 @@ +/* Copyright (c) 2026 The Brave Authors. All rights reserved. + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at https://mozilla.org/MPL/2.0/. */ + +#include "brave/components/p3a_utils/event_relay.h" + +namespace p3a_utils { + +// static +EventRelay* EventRelay::GetInstance() { + static base::NoDestructor instance; + return instance.get(); +} + +EventRelay::EventRelay() = default; +EventRelay::~EventRelay() = default; + +void EventRelay::AddObserver(Observer* observer) { + observers_.AddObserver(observer); +} + +void EventRelay::RemoveObserver(Observer* observer) { + observers_.RemoveObserver(observer); +} + +void EventRelay::NotifyCustomAttributeSet( + std::string_view attribute_name, + std::optional attribute_value) { + observers_.Notify(&Observer::OnCustomAttributeSet, attribute_name, + attribute_value); +} + +} // namespace p3a_utils diff --git a/components/p3a_utils/event_relay.h b/components/p3a_utils/event_relay.h new file mode 100644 index 00000000000..278cd85f371 --- /dev/null +++ b/components/p3a_utils/event_relay.h @@ -0,0 +1,50 @@ +/* Copyright (c) 2026 The Brave Authors. All rights reserved. + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at https://mozilla.org/MPL/2.0/. */ + +#ifndef BRAVE_COMPONENTS_P3A_UTILS_EVENT_RELAY_H_ +#define BRAVE_COMPONENTS_P3A_UTILS_EVENT_RELAY_H_ + +#include +#include + +#include "base/no_destructor.h" +#include "base/observer_list.h" +#include "base/observer_list_types.h" + +namespace p3a_utils { + +class EventRelay { + public: + class Observer : public base::CheckedObserver { + public: + virtual void OnCustomAttributeSet( + std::string_view attribute_name, + std::optional attribute_value) = 0; + }; + + static EventRelay* GetInstance(); + + EventRelay(const EventRelay&) = delete; + EventRelay& operator=(const EventRelay&) = delete; + + void AddObserver(Observer* observer); + void RemoveObserver(Observer* observer); + + void NotifyCustomAttributeSet( + std::string_view attribute_name, + std::optional attribute_value); + + private: + friend class base::NoDestructor; + + EventRelay(); + ~EventRelay(); + + base::ObserverList observers_; +}; + +} // namespace p3a_utils + +#endif // BRAVE_COMPONENTS_P3A_UTILS_EVENT_RELAY_H_