puma: Implement PumaService for sending PUMA metrics

PumaService is responsible for uploading PUMA histograms to the Private
Metrics endpoint.  It does so periodically, similarly to other metrics
services (e.g. DWA).

Bug: b:452034784, b:456094891
Change-Id: Iba78534938f759873acb7567d48af2421a50a221
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7176328
Reviewed-by: Alexei Svitkine <asvitkine@chromium.org>
Commit-Queue: Kamil Jarosz <kjarosz@google.com>
Reviewed-by: Luc Nguyen <lucnguyen@google.com>
Reviewed-by: Matt Dembski <dembski@google.com>
Cr-Commit-Position: refs/heads/main@{#1551654}
NOKEYCHECK=True
GitOrigin-RevId: 4162fa81a003df8e715e77ee64a7410186727e5c
diff --git a/private_metrics/BUILD.gn b/private_metrics/BUILD.gn
index 81b9cc1..033a665 100644
--- a/private_metrics/BUILD.gn
+++ b/private_metrics/BUILD.gn
@@ -64,7 +64,6 @@
     "dkm_entry_builder.h",
     "dkm_entry_builder_base.cc",
     "dkm_entry_builder_base.h",
-    "private_metrics_pref_names.cc",
     "private_metrics_pref_names.h",
     "private_metrics_reporting_service.cc",
     "private_metrics_reporting_service.h",
@@ -72,6 +71,8 @@
     "private_metrics_unsent_log_store_metrics.h",
     "puma_histogram_encoder.cc",
     "puma_histogram_encoder.h",
+    "puma_service.cc",
+    "puma_service.h",
   ]
   public_deps = [
     ":dwa_recorder",
@@ -107,6 +108,7 @@
     "//components/metrics/dwa/dwa_recorder_unittest.cc",
     "//components/metrics/dwa/dwa_service_unittest.cc",
     "//components/metrics/private_metrics/puma_histogram_encoder_unittest.cc",
+    "//components/metrics/private_metrics/puma_service_unittest.cc",
     "data_upload_config_downloader_unittest.cc",
     "dkm_entry_builder_unittest.cc",
   ]
diff --git a/private_metrics/private_metrics_features.cc b/private_metrics/private_metrics_features.cc
index 05c6c91..46b6422 100644
--- a/private_metrics/private_metrics_features.cc
+++ b/private_metrics/private_metrics_features.cc
@@ -8,4 +8,18 @@
 
 BASE_FEATURE(kPrivateMetricsFeature, base::FEATURE_DISABLED_BY_DEFAULT);
 
+BASE_FEATURE(kPrivateMetricsPuma, base::FEATURE_DISABLED_BY_DEFAULT);
+
+const base::FeatureParam<size_t> kPrivateMetricsPumaMinLogQueueCount{
+    &kPrivateMetricsPuma, "puma_min_log_queue_count", 10};
+
+const base::FeatureParam<size_t> kPrivateMetricsPumaMinLogQueueSizeBytes{
+    &kPrivateMetricsPuma, "puma_min_log_queue_size_bytes",
+    300 * 1024};  // 300 KiB
+
+const base::FeatureParam<size_t> kPrivateMetricsPumaMaxLogSizeBytes{
+    &kPrivateMetricsPuma, "puma_max_log_size_bytes", 1024 * 1024};  // 1 MiB
+
+BASE_FEATURE(kPrivateMetricsPumaRc, base::FEATURE_DISABLED_BY_DEFAULT);
+
 }  // namespace metrics::private_metrics
diff --git a/private_metrics/private_metrics_features.h b/private_metrics/private_metrics_features.h
index 2b5a031..3d894df 100644
--- a/private_metrics/private_metrics_features.h
+++ b/private_metrics/private_metrics_features.h
@@ -7,6 +7,7 @@
 
 #include "base/component_export.h"
 #include "base/feature_list.h"
+#include "base/metrics/field_trial_params.h"
 
 namespace metrics::private_metrics {
 
@@ -15,6 +16,23 @@
 // go/chrome-trusted-private-metrics and go/etld-plus-one-metrics.
 BASE_DECLARE_FEATURE(kPrivateMetricsFeature);
 
+// Enables Private UMA service. When enabled, PUMA histograms will be reported
+// to the Private Metrics endpoint.
+//
+// Note: it's likely this is not the only feature you want to enable, as a
+// specific PUMA type can also be implemented behind a feature.
+BASE_DECLARE_FEATURE(kPrivateMetricsPuma);
+
+// The following feature params are used to parameterize unsent log store
+// limits for PUMA. See UnsentLogStoreLimits.
+extern const base::FeatureParam<size_t> kPrivateMetricsPumaMinLogQueueCount;
+extern const base::FeatureParam<size_t> kPrivateMetricsPumaMinLogQueueSizeBytes;
+extern const base::FeatureParam<size_t> kPrivateMetricsPumaMaxLogSizeBytes;
+
+// Enables Private UMA for Regional Capabilities. Enabling this feature will
+// collect and uploads logs of this type of PUMA.
+BASE_DECLARE_FEATURE(kPrivateMetricsPumaRc);
+
 }  // namespace metrics::private_metrics
 
 #endif  // COMPONENTS_METRICS_PRIVATE_METRICS_PRIVATE_METRICS_FEATURES_H_
diff --git a/private_metrics/private_metrics_pref_names.cc b/private_metrics/private_metrics_pref_names.cc
deleted file mode 100644
index 1f815d5..0000000
--- a/private_metrics/private_metrics_pref_names.cc
+++ /dev/null
@@ -1,11 +0,0 @@
-// Copyright 2025 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/private_metrics/private_metrics_pref_names.h"
-
-namespace metrics::private_metrics::prefs {
-
-const char kUnsentLogStoreName[] = "private_metrics.persistent_logs";
-
-}  // namespace metrics::private_metrics::prefs
diff --git a/private_metrics/private_metrics_pref_names.h b/private_metrics/private_metrics_pref_names.h
index d0e4ed2..f67f8df 100644
--- a/private_metrics/private_metrics_pref_names.h
+++ b/private_metrics/private_metrics_pref_names.h
@@ -8,7 +8,10 @@
 namespace metrics::private_metrics::prefs {
 
 // Preference which stores serialized private metrics logs to be uploaded.
-extern const char kUnsentLogStoreName[];
+inline constexpr char kUnsentLogStoreName[] = "private_metrics.persistent_logs";
+
+// Preference which stores client_id for PUMA Regional Capabilities.
+inline constexpr char kPumaRcClientId[] = "private_metrics.puma.client_id.rc";
 
 }  // namespace metrics::private_metrics::prefs
 
diff --git a/private_metrics/puma_service.cc b/private_metrics/puma_service.cc
new file mode 100644
index 0000000..31346a9
--- /dev/null
+++ b/private_metrics/puma_service.cc
@@ -0,0 +1,247 @@
+// Copyright 2025 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/private_metrics/puma_service.h"
+
+#include <optional>
+
+#include "base/metrics/puma_histogram_functions.h"
+#include "base/rand_util.h"
+#include "base/version.h"
+#include "components/metrics/metrics_service_client.h"
+#include "components/metrics/private_metrics/private_metrics_features.h"
+#include "components/metrics/private_metrics/private_metrics_pref_names.h"
+#include "components/metrics/private_metrics/puma_histogram_encoder.h"
+#include "components/version_info/version_info.h"
+#include "third_party/metrics_proto/private_metrics/private_metrics.pb.h"
+#include "third_party/metrics_proto/private_metrics/private_user_metrics.pb.h"
+#include "third_party/metrics_proto/private_metrics/system_profiles/coarse_system_profile.pb.h"
+#include "third_party/metrics_proto/private_metrics/system_profiles/rc_coarse_system_profile.pb.h"
+
+namespace metrics::private_metrics {
+
+namespace {
+
+// TODO(b/463573197): Unify namespaces used for code and protos
+using ::private_metrics::Platform;
+using ::private_metrics::PrivateMetricEndpointPayload;
+using ::private_metrics::PrivateUserMetrics;
+using ::private_metrics::RcCoarseSystemProfile;
+using ::private_metrics::RcCoarseSystemProfile_Channel;
+
+// Retrieves the storage parameters to control the reporting service.
+UnsentLogStore::UnsentLogStoreLimits GetLogStoreLimits() {
+  return UnsentLogStore::UnsentLogStoreLimits{
+      .min_log_count = kPrivateMetricsPumaMinLogQueueCount.Get(),
+      .min_queue_size_bytes = kPrivateMetricsPumaMinLogQueueSizeBytes.Get(),
+      .max_log_size_bytes = kPrivateMetricsPumaMaxLogSizeBytes.Get(),
+  };
+}
+
+PrivateMetricEndpointPayload BuildPrivateMetricEndpointPayload(
+    PrivateUserMetrics private_uma_report) {
+  PrivateMetricEndpointPayload payload;
+  payload.set_report_type(PrivateMetricEndpointPayload::PUMA_RC);
+  payload.mutable_private_uma_report()->Swap(&private_uma_report);
+  return payload;
+}
+
+RcCoarseSystemProfile_Channel MapChannelToRcChannel(
+    SystemProfileProto::Channel channel) {
+  switch (channel) {
+    case SystemProfileProto::CHANNEL_CANARY:
+      return RcCoarseSystemProfile::CHANNEL_CANARY;
+    case SystemProfileProto::CHANNEL_DEV:
+      return RcCoarseSystemProfile::CHANNEL_DEV;
+    case SystemProfileProto::CHANNEL_BETA:
+      return RcCoarseSystemProfile::CHANNEL_BETA;
+    case SystemProfileProto::CHANNEL_STABLE:
+      return RcCoarseSystemProfile::CHANNEL_STABLE;
+    default:
+      return RcCoarseSystemProfile::CHANNEL_UNKNOWN;
+  }
+}
+
+Platform GetCurrentPlatform() {
+#if BUILDFLAG(IS_WIN)
+  return Platform::PLATFORM_WINDOWS;
+#elif BUILDFLAG(IS_MAC)
+  return Platform::PLATFORM_MACOS;
+#elif BUILDFLAG(IS_LINUX)
+  return Platform::PLATFORM_LINUX;
+#elif BUILDFLAG(IS_ANDROID)
+  // TODO(b/463580425): Differentiate between Android platforms.
+  return Platform::PLATFORM_ANDROID;
+#elif BUILDFLAG(IS_IOS)
+  return Platform::PLATFORM_IOS;
+#elif BUILDFLAG(IS_CHROMEOS)
+  return Platform::PLATFORM_CHROMEOS;
+#else
+  return Platform::PLATFORM_OTHER;
+#endif
+}
+
+}  // namespace
+
+PumaService::PumaService(MetricsServiceClient* client, PrefService* local_state)
+    : client_(client),
+      local_state_(local_state),
+      reporting_service_(client, local_state, GetLogStoreLimits(), false) {
+  reporting_service_.Initialize();
+
+  // Set up the scheduler for PumaService.
+  auto rotate_callback = base::BindRepeating(&PumaService::RotateLog,
+                                             self_ptr_factory_.GetWeakPtr());
+  auto get_upload_interval_callback = base::BindRepeating(
+      &MetricsServiceClient::GetUploadInterval, base::Unretained(client_));
+  bool fast_startup = client_->ShouldStartUpFast();
+  scheduler_ = std::make_unique<metrics::MetricsRotationScheduler>(
+      rotate_callback, get_upload_interval_callback, fast_startup);
+  scheduler_->InitTaskComplete();
+}
+
+PumaService::~PumaService() {
+  DisableReporting();
+}
+
+PrivateMetricsReportingService* PumaService::reporting_service() {
+  return &reporting_service_;
+}
+
+void PumaService::EnableReporting() {
+  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
+  if (reporting_service_.reporting_active()) {
+    return;
+  }
+
+  scheduler_->Start();
+  reporting_service_.EnableReporting();
+}
+
+void PumaService::DisableReporting() {
+  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
+  if (!reporting_service_.reporting_active()) {
+    return;
+  }
+
+  reporting_service_.DisableReporting();
+  scheduler_->Stop();
+  Flush(metrics::MetricsLogsEventManager::CreateReason::kServiceShutdown);
+}
+
+void PumaService::Flush(metrics::MetricsLogsEventManager::CreateReason reason) {
+  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
+
+  BuildPrivateMetricRcReportAndStoreLog(reason);
+  reporting_service_.unsent_log_store()->TrimAndPersistUnsentLogs(true);
+}
+
+// static
+bool PumaService::IsPumaEnabled() {
+  if (base::FeatureList::IsEnabled(kPrivateMetricsFeature)) {
+    // Note: PUMA should never be enabled with Private Metrics, as PUMA's
+    // implementation assumes that the Private Metrics feature is disabled.
+    return false;
+  }
+
+  return base::FeatureList::IsEnabled(kPrivateMetricsPuma);
+}
+
+// static
+void PumaService::RegisterPrefs(PrefRegistrySimple* registry) {
+  registry->RegisterUint64Pref(prefs::kPumaRcClientId, 0u);
+  // PrivateMetricsReportingService prefs are registered already by DWA.
+}
+
+std::optional<uint64_t> PumaService::GetPumaRcClientId() {
+  if (!base::FeatureList::IsEnabled(kPrivateMetricsPumaRc)) {
+    // This is a failsafe mechanism that prevents reporting IDs in case the
+    // feature is disabled.
+    return std::nullopt;
+  }
+
+  uint64_t client_id = local_state_->GetUint64(prefs::kPumaRcClientId);
+
+  // Generate the ID in case it's not available.
+  if (client_id == 0u) {
+    while (client_id == 0u) {
+      client_id = base::RandUint64();
+    }
+    local_state_->SetUint64(prefs::kPumaRcClientId, client_id);
+  }
+
+  return client_id;
+}
+
+void PumaService::RotateLog() {
+  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
+  if (!reporting_service_.unsent_log_store()->has_unsent_logs()) {
+    BuildPrivateMetricRcReportAndStoreLog(
+        metrics::MetricsLogsEventManager::CreateReason::kPeriodic);
+  }
+  reporting_service_.Start();
+  scheduler_->RotationFinished();
+}
+
+void PumaService::RecordRcProfile(RcCoarseSystemProfile* rc_profile) {
+  rc_profile->set_channel(MapChannelToRcChannel(client_->GetChannel()));
+
+  rc_profile->set_is_extended_stable_channel(
+      client_->IsExtendedStableChannel());
+
+  rc_profile->set_platform(GetCurrentPlatform());
+  rc_profile->set_milestone(version_info::GetMajorVersionNumberAsInt());
+}
+
+std::optional<::private_metrics::PrivateUserMetrics>
+PumaService::BuildPrivateMetricRcReport() {
+  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
+
+  if (!base::FeatureList::IsEnabled(kPrivateMetricsPumaRc)) {
+    return std::nullopt;
+  }
+
+  ::private_metrics::PrivateUserMetrics report;
+  PumaHistogramEncoder::EncodeHistogramDeltas(base::PumaType::kRc, report);
+
+  if (report.histogram_events_size() == 0) {
+    // No histograms to report.
+    return std::nullopt;
+  }
+
+  std::optional<uint64_t> client_id = GetPumaRcClientId();
+  if (!client_id.has_value()) {
+    return std::nullopt;
+  }
+  report.set_client_id(client_id.value());
+  RecordRcProfile(report.mutable_rc_profile());
+
+  return std::move(report);
+}
+
+void PumaService::BuildPrivateMetricRcReportAndStoreLog(
+    metrics::MetricsLogsEventManager::CreateReason reason) {
+  std::optional<::private_metrics::PrivateUserMetrics> report =
+      BuildPrivateMetricRcReport();
+
+  if (!report) {
+    // No report to store.
+    return;
+  }
+
+  auto payload = BuildPrivateMetricEndpointPayload(*std::move(report));
+
+  std::string serialized_log;
+  if (!payload.SerializeToString(&serialized_log)) {
+    // Drop the report in case serialization fails.
+    // TODO(b/463571627): Add a histogram
+    return;
+  }
+
+  LogMetadata metadata;
+  reporting_service_.unsent_log_store()->StoreLog(serialized_log, metadata,
+                                                  reason);
+}
+
+}  // namespace metrics::private_metrics
diff --git a/private_metrics/puma_service.h b/private_metrics/puma_service.h
new file mode 100644
index 0000000..ad78dce
--- /dev/null
+++ b/private_metrics/puma_service.h
@@ -0,0 +1,110 @@
+// Copyright 2025 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef COMPONENTS_METRICS_PRIVATE_METRICS_PUMA_SERVICE_H_
+#define COMPONENTS_METRICS_PRIVATE_METRICS_PUMA_SERVICE_H_
+
+#include "components/metrics/metrics_rotation_scheduler.h"
+#include "components/metrics/metrics_service_client.h"
+#include "components/metrics/private_metrics/private_metrics_reporting_service.h"
+#include "components/prefs/pref_service.h"
+#include "third_party/metrics_proto/private_metrics/private_user_metrics.pb.h"
+
+namespace private_metrics {
+class RcCoarseSystemProfile;
+}
+
+namespace metrics::private_metrics {
+
+// PumaService is responsible for uploading Private UMA histograms.
+class PumaService {
+ public:
+  PumaService(MetricsServiceClient* client, PrefService* pref_service);
+  PumaService(const PumaService&) = delete;
+  PumaService& operator=(const PumaService&) = delete;
+
+  ~PumaService();
+
+  PrivateMetricsReportingService* reporting_service();
+
+  void EnableReporting();
+  void DisableReporting();
+
+  // Flushes any event currently in the recorder to prefs.
+  void Flush(metrics::MetricsLogsEventManager::CreateReason reason);
+
+  // Returns true if the PUMA feature is enabled, false otherwise.
+  static bool IsPumaEnabled();
+
+  static void RegisterPrefs(PrefRegistrySimple* registry);
+
+ private:
+  FRIEND_TEST_ALL_PREFIXES(PumaServiceRcTest,
+                           RcBuildReportAndStore_DoesCreateAndStoreReport);
+  FRIEND_TEST_ALL_PREFIXES(PumaServiceRcTest,
+                           RcBuildReport_DoesCreateReportWithEvents);
+  FRIEND_TEST_ALL_PREFIXES(PumaServiceRcTest,
+                           RcBuildReport_DoesNotCreateReportWithoutEvents);
+  FRIEND_TEST_ALL_PREFIXES(PumaServiceRcTest,
+                           RcBuildReport_PayloadProperlyFilled);
+  FRIEND_TEST_ALL_PREFIXES(PumaServiceRcTest, RcClientId_IsNonNull);
+  FRIEND_TEST_ALL_PREFIXES(PumaServiceRcTest,
+                           RcClientId_SameWhenExecutedMultipleTimes);
+  FRIEND_TEST_ALL_PREFIXES(PumaServiceRcTest, RcClientId_UpdatesPref);
+  FRIEND_TEST_ALL_PREFIXES(PumaServiceRcTest, RcRecordCoarseSystemProfile);
+  FRIEND_TEST_ALL_PREFIXES(
+      PumaServiceTest,
+      RcBuildReport_DoesNotCreateReportWithFeatureDisabled);
+  FRIEND_TEST_ALL_PREFIXES(PumaServiceTest,
+                           RcClientId_IsNullWhenPumaRcIsDisabled);
+
+  // Gets or generates client ID for PUMA for Regional Capabilities.
+  //
+  // This client ID is persistent and not joinable with any other client ID.
+  // `std::nullopt` is returned when the feature is disabled, and IDs should not
+  // be collected.
+  std::optional<uint64_t> GetPumaRcClientId();
+
+  // Records coarse system profile for PUMA for Regional Capabilities.
+  void RecordRcProfile(::private_metrics::RcCoarseSystemProfile* rc_profile);
+
+  // Constructs a new PrivateMetricReport from available data from PUMA for
+  // Regional Capabilities, and returns it.
+  //
+  // Marks the data as processed, and makes sure it will not be processed again.
+  std::optional<::private_metrics::PrivateUserMetrics>
+  BuildPrivateMetricRcReport();
+
+  // Constructs a new PrivateMetricReport from available data from PUMA for
+  // Regional Capabilities, and stores it in the unsent log store.
+  //
+  // Marks the data as processed, and makes sure it will not be processed again.
+  void BuildPrivateMetricRcReportAndStoreLog(
+      metrics::MetricsLogsEventManager::CreateReason reason);
+
+  // Periodically called by `scheduler_` to advance processing of logs.
+  void RotateLog();
+
+  SEQUENCE_CHECKER(sequence_checker_);
+
+  // The metrics client `this` is service is associated with.
+  raw_ptr<MetricsServiceClient> client_;
+
+  // Local state; preferences not associated with a specific profile.
+  raw_ptr<PrefService> local_state_;
+
+  // Service for uploading serialized logs to Private Metrics endpoint.
+  PrivateMetricsReportingService reporting_service_;
+
+  // The scheduler for determining when uploads should happen.
+  std::unique_ptr<MetricsRotationScheduler> scheduler_;
+
+  // Weak pointers factory used to post task on different threads. All weak
+  // pointers managed by this factory have the same lifetime as PumaService.
+  base::WeakPtrFactory<PumaService> self_ptr_factory_{this};
+};
+
+}  // namespace metrics::private_metrics
+
+#endif  // COMPONENTS_METRICS_PRIVATE_METRICS_PUMA_SERVICE_H_
diff --git a/private_metrics/puma_service_unittest.cc b/private_metrics/puma_service_unittest.cc
new file mode 100644
index 0000000..c511d3a
--- /dev/null
+++ b/private_metrics/puma_service_unittest.cc
@@ -0,0 +1,395 @@
+// Copyright 2025 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/private_metrics/puma_service.h"
+
+#include <memory>
+
+#include "base/metrics/puma_histogram_functions.h"
+#include "base/test/metrics/histogram_tester.h"
+#include "base/test/scoped_feature_list.h"
+#include "base/test/task_environment.h"
+#include "components/metrics/private_metrics/private_metrics_features.h"
+#include "components/metrics/private_metrics/private_metrics_pref_names.h"
+#include "components/metrics/test/test_metrics_service_client.h"
+#include "components/prefs/testing_pref_service.h"
+#include "testing/gtest/include/gtest/gtest.h"
+#include "third_party/metrics_proto/private_metrics/system_profiles/coarse_system_profile.pb.h"
+#include "third_party/metrics_proto/private_metrics/system_profiles/rc_coarse_system_profile.pb.h"
+
+namespace metrics::private_metrics {
+
+namespace {
+
+using ::private_metrics::RcCoarseSystemProfile;
+
+class PumaServiceTest : public testing::Test {
+ public:
+  PumaServiceTest() = default;
+  PumaServiceTest(const PumaServiceTest&) = delete;
+  PumaServiceTest& operator=(const PumaServiceTest&) = delete;
+  ~PumaServiceTest() override = default;
+
+  void SetUp() override {
+    scoped_feature_list_.InitWithFeatures(GetEnabledFeatures(), {});
+
+    PumaService::RegisterPrefs(prefs_.registry());
+    PrivateMetricsReportingService::RegisterPrefs(prefs_.registry());
+
+    puma_service_ = std::make_unique<PumaService>(&client_, &prefs_);
+  }
+
+  void TearDown() override { puma_service_ = nullptr; }
+
+  virtual std::vector<base::test::FeatureRef> GetEnabledFeatures() {
+    return {kPrivateMetricsPuma};
+  }
+
+  size_t GetUnsentLogCount() {
+    return puma_service_->reporting_service()->unsent_log_store()->size();
+  }
+
+  size_t GetPersistedLogCount() {
+    return prefs_.GetList(prefs::kUnsentLogStoreName).size();
+  }
+
+ protected:
+  TestMetricsServiceClient client_;
+  TestingPrefServiceSimple prefs_;
+  base::test::ScopedFeatureList scoped_feature_list_;
+
+  std::unique_ptr<PumaService> puma_service_;
+
+  base::test::TaskEnvironment task_environment_{
+      base::test::TaskEnvironment::MainThreadType::UI,
+      base::test::TaskEnvironment::ThreadPoolExecutionMode::QUEUED,
+      base::test::TaskEnvironment::TimeSource::MOCK_TIME};
+};
+
+class PumaServiceFeaturesTest
+    : public PumaServiceTest,
+      public testing::WithParamInterface<
+          std::tuple<std::vector<base::test::FeatureRef>, bool>> {
+ public:
+  PumaServiceFeaturesTest() = default;
+
+  PumaServiceFeaturesTest(const PumaServiceFeaturesTest&) = delete;
+  PumaServiceFeaturesTest& operator=(const PumaServiceFeaturesTest&) = delete;
+
+  ~PumaServiceFeaturesTest() override = default;
+
+  std::vector<base::test::FeatureRef> GetEnabledFeatures() override {
+    return std::get<0>(GetParam());
+  }
+};
+
+class PumaServiceRcTest : public PumaServiceTest {
+ public:
+  PumaServiceRcTest() = default;
+
+  PumaServiceRcTest(const PumaServiceRcTest&) = delete;
+  PumaServiceRcTest& operator=(const PumaServiceRcTest&) = delete;
+
+  ~PumaServiceRcTest() override = default;
+
+  std::vector<base::test::FeatureRef> GetEnabledFeatures() override {
+    return {kPrivateMetricsPuma, kPrivateMetricsPumaRc};
+  }
+};
+
+}  // namespace
+
+INSTANTIATE_TEST_SUITE_P(
+    All,
+    PumaServiceFeaturesTest,
+    testing::ValuesIn(
+        std::vector<std::tuple<std::vector<base::test::FeatureRef>, bool>>{
+            {{}, false},
+            {{kPrivateMetricsFeature}, false},
+            {{kPrivateMetricsPuma}, true},
+            {{kPrivateMetricsPuma, kPrivateMetricsFeature}, false},
+        }));
+
+TEST_P(PumaServiceFeaturesTest, IsPumaEnabled) {
+  EXPECT_EQ(PumaService::IsPumaEnabled(), std::get<1>(GetParam()));
+}
+
+TEST_F(PumaServiceRcTest, RcRecordCoarseSystemProfile) {
+  client_.set_is_extended_stable_channel(true);
+
+  RcCoarseSystemProfile rc_profile;
+
+  puma_service_->RecordRcProfile(&rc_profile);
+
+  EXPECT_TRUE(rc_profile.has_channel());
+  EXPECT_EQ(rc_profile.channel(), RcCoarseSystemProfile::CHANNEL_BETA);
+
+  EXPECT_TRUE(rc_profile.has_is_extended_stable_channel());
+  EXPECT_EQ(rc_profile.is_extended_stable_channel(), true);
+
+  EXPECT_TRUE(rc_profile.has_milestone());
+  EXPECT_TRUE(rc_profile.has_platform());
+
+  // TODO(b:452034784) Implement profile_country_id mapping.
+  EXPECT_FALSE(rc_profile.has_profile_country_id());
+}
+
+TEST_F(PumaServiceTest, RcClientId_IsNullWhenPumaRcIsDisabled) {
+  EXPECT_FALSE(puma_service_->GetPumaRcClientId().has_value());
+}
+
+TEST_F(PumaServiceRcTest, RcClientId_IsNonNull) {
+  EXPECT_TRUE(puma_service_->GetPumaRcClientId().has_value());
+}
+
+TEST_F(PumaServiceRcTest, RcClientId_SameWhenExecutedMultipleTimes) {
+  std::optional<uint64_t> client_id_1 = puma_service_->GetPumaRcClientId();
+  std::optional<uint64_t> client_id_2 = puma_service_->GetPumaRcClientId();
+
+  EXPECT_TRUE(client_id_1.has_value());
+  EXPECT_EQ(client_id_1, client_id_2);
+}
+
+TEST_F(PumaServiceRcTest, RcClientId_UpdatesPref) {
+  EXPECT_EQ(prefs_.GetUint64(prefs::kPumaRcClientId), 0u);
+
+  std::optional<uint64_t> client_id_1 = puma_service_->GetPumaRcClientId();
+  uint64_t pref_value_1 = prefs_.GetUint64(prefs::kPumaRcClientId);
+
+  EXPECT_NE(pref_value_1, 0u);
+  EXPECT_EQ(client_id_1, pref_value_1);
+
+  std::optional<uint64_t> client_id_2 = puma_service_->GetPumaRcClientId();
+  uint64_t pref_value_2 = prefs_.GetUint64(prefs::kPumaRcClientId);
+
+  EXPECT_EQ(client_id_2, pref_value_2);
+  EXPECT_EQ(pref_value_1, pref_value_2);
+}
+
+TEST_F(PumaServiceRcTest, RcBuildReport_DoesNotCreateReportWithoutEvents) {
+  auto report = puma_service_->BuildPrivateMetricRcReport();
+  EXPECT_FALSE(report.has_value());
+}
+
+TEST_F(PumaServiceRcTest, RcBuildReport_DoesCreateReportWithEvents) {
+  base::PumaHistogramBoolean(base::PumaType::kRc,
+                             "PUMA.PumaServiceTestHistogram.Boolean1", true);
+
+  auto report = puma_service_->BuildPrivateMetricRcReport();
+  EXPECT_TRUE(report.has_value());
+}
+
+TEST_F(PumaServiceTest, RcBuildReport_DoesNotCreateReportWithFeatureDisabled) {
+  base::PumaHistogramBoolean(base::PumaType::kRc,
+                             "PUMA.PumaServiceTestHistogram.Boolean2", true);
+
+  auto report = puma_service_->BuildPrivateMetricRcReport();
+  EXPECT_FALSE(report.has_value());
+}
+
+TEST_F(PumaServiceRcTest, RcBuildReport_PayloadProperlyFilled) {
+  base::PumaHistogramExactLinear(
+      base::PumaType::kRc, "PUMA.PumaServiceTestHistogram.Linear1", 12, 100);
+
+  auto report = puma_service_->BuildPrivateMetricRcReport();
+
+  EXPECT_TRUE(report.has_value());
+
+  EXPECT_TRUE(report->has_client_id());
+  EXPECT_NE(report->client_id(), 0u);
+
+  EXPECT_TRUE(report->has_rc_profile());
+
+  EXPECT_EQ(report->histogram_events_size(), 1);
+
+  auto histogram_event = report->histogram_events().at(0);
+  EXPECT_TRUE(histogram_event.has_name_hash());
+  EXPECT_EQ(histogram_event.bucket_size(), 1);
+}
+
+TEST_F(PumaServiceRcTest, RcBuildReportAndStore_DoesCreateAndStoreReport) {
+  base::PumaHistogramBoolean(base::PumaType::kRc,
+                             "PUMA.PumaServiceTestHistogram.Boolean3", true);
+
+  EXPECT_EQ(GetUnsentLogCount(), 0u);
+
+  puma_service_->BuildPrivateMetricRcReportAndStoreLog(
+      metrics::MetricsLogsEventManager::CreateReason::kPeriodic);
+
+  EXPECT_EQ(GetUnsentLogCount(), 1u);
+}
+
+TEST_F(PumaServiceRcTest, RcLogsArePersistedAfterFlush) {
+  base::PumaHistogramBoolean(base::PumaType::kRc,
+                             "PUMA.PumaServiceTestHistogram.Boolean4", true);
+
+  EXPECT_EQ(GetPersistedLogCount(), 0u);
+  puma_service_->Flush(
+      metrics::MetricsLogsEventManager::CreateReason::kPeriodic);
+  EXPECT_EQ(GetPersistedLogCount(), 1u);
+}
+
+TEST_F(PumaServiceRcTest, RcLogsArePersistedAfterDisablingReporting) {
+  puma_service_->EnableReporting();
+
+  base::PumaHistogramBoolean(base::PumaType::kRc,
+                             "PUMA.PumaServiceTestHistogram.Boolean5", true);
+
+  EXPECT_EQ(GetPersistedLogCount(), 0u);
+  puma_service_->DisableReporting();
+  EXPECT_EQ(GetPersistedLogCount(), 1u);
+}
+
+TEST_F(PumaServiceRcTest,
+       RcLogsAreNotPersistedAfterDisablingReportingWhenReportingWasDisabled) {
+  base::PumaHistogramBoolean(base::PumaType::kRc,
+                             "PUMA.PumaServiceTestHistogram.Boolean6", true);
+
+  EXPECT_EQ(GetPersistedLogCount(), 0u);
+  puma_service_->DisableReporting();
+  EXPECT_EQ(GetPersistedLogCount(), 0u);
+}
+
+TEST_F(PumaServiceRcTest, RcLogsArePersistedAfterDestruction) {
+  puma_service_->EnableReporting();
+
+  base::PumaHistogramBoolean(base::PumaType::kRc,
+                             "PUMA.PumaServiceTestHistogram.Boolean7", true);
+
+  EXPECT_EQ(GetPersistedLogCount(), 0u);
+  puma_service_ = nullptr;
+  EXPECT_EQ(GetPersistedLogCount(), 1u);
+}
+
+TEST_F(PumaServiceRcTest,
+       RcLogsAreNotPersistedAfterDestructionWhenReportingWasInactive) {
+  base::PumaHistogramBoolean(base::PumaType::kRc,
+                             "PUMA.PumaServiceTestHistogram.Boolean8", true);
+
+  EXPECT_EQ(GetPersistedLogCount(), 0u);
+  puma_service_ = nullptr;
+  EXPECT_EQ(GetPersistedLogCount(), 0u);
+}
+
+TEST_F(PumaServiceTest, EnableReportingStartsSchedulerTask) {
+  EXPECT_EQ(task_environment_.GetPendingMainThreadTaskCount(), 0u);
+
+  // When the reporting is enabled, we should have:
+  //  1. the log rotation scheduler, and
+  //  2. the uploader.
+  puma_service_->EnableReporting();
+  EXPECT_EQ(task_environment_.GetPendingMainThreadTaskCount(), 2u);
+
+  puma_service_->DisableReporting();
+  EXPECT_EQ(task_environment_.GetPendingMainThreadTaskCount(), 0u);
+}
+
+TEST_F(PumaServiceTest, EnableReportingTwiceDoesNotStartAdditionalTasks) {
+  EXPECT_EQ(task_environment_.GetPendingMainThreadTaskCount(), 0u);
+
+  puma_service_->EnableReporting();
+  EXPECT_EQ(task_environment_.GetPendingMainThreadTaskCount(), 2u);
+
+  puma_service_->EnableReporting();
+  EXPECT_EQ(task_environment_.GetPendingMainThreadTaskCount(), 2u);
+}
+
+TEST_F(PumaServiceRcTest, UploadUnsentLogs) {
+  puma_service_->EnableReporting();
+
+  base::PumaHistogramBoolean(base::PumaType::kRc,
+                             "PUMA.PumaServiceTestHistogram.Boolean9", true);
+
+  EXPECT_EQ(GetUnsentLogCount(), 0u);
+  EXPECT_EQ(GetPersistedLogCount(), 0u);
+
+  base::TimeDelta upload_interval = client_.GetUploadInterval();
+  task_environment_.FastForwardBy(upload_interval);
+
+  EXPECT_EQ(GetUnsentLogCount(), 1u);
+  EXPECT_EQ(GetPersistedLogCount(), 0u);
+
+  EXPECT_NE(client_.uploader(), nullptr);
+  EXPECT_TRUE(client_.uploader()->is_uploading());
+
+  // Simulate logs upload.
+  client_.uploader()->CompleteUpload(200);
+  EXPECT_FALSE(client_.uploader()->is_uploading());
+
+  EXPECT_EQ(GetUnsentLogCount(), 0u);
+  EXPECT_EQ(GetPersistedLogCount(), 0u);
+}
+
+TEST_F(PumaServiceRcTest, UploadPersistedLogs) {
+  puma_service_->EnableReporting();
+
+  base::PumaHistogramBoolean(base::PumaType::kRc,
+                             "PUMA.PumaServiceTestHistogram.Boolean10", true);
+
+  EXPECT_EQ(GetUnsentLogCount(), 0u);
+  EXPECT_EQ(GetPersistedLogCount(), 0u);
+
+  // Simulate forced shutdown
+  puma_service_ = nullptr;
+
+  EXPECT_EQ(GetPersistedLogCount(), 1u);
+
+  puma_service_ = std::make_unique<PumaService>(&client_, &prefs_);
+
+  EXPECT_EQ(GetUnsentLogCount(), 1u);
+  EXPECT_EQ(GetPersistedLogCount(), 1u);
+
+  puma_service_->EnableReporting();
+
+  base::TimeDelta upload_interval = client_.GetUploadInterval();
+  task_environment_.FastForwardBy(upload_interval);
+
+  EXPECT_EQ(GetUnsentLogCount(), 1u);
+  EXPECT_EQ(GetPersistedLogCount(), 1u);
+
+  EXPECT_NE(client_.uploader(), nullptr);
+  EXPECT_TRUE(client_.uploader()->is_uploading());
+
+  // Simulate logs upload.
+  client_.uploader()->CompleteUpload(200);
+  EXPECT_FALSE(client_.uploader()->is_uploading());
+
+  EXPECT_EQ(GetUnsentLogCount(), 0u);
+  EXPECT_EQ(GetPersistedLogCount(), 0u);
+
+  EXPECT_EQ(task_environment_.GetPendingMainThreadTaskCount(), 1u);
+}
+
+TEST_F(PumaServiceRcTest, LogsUploadedPeriodically) {
+  puma_service_->EnableReporting();
+
+  base::PumaHistogramBoolean(base::PumaType::kRc,
+                             "PUMA.PumaServiceTestHistogram.Boolean11", true);
+
+  EXPECT_EQ(GetUnsentLogCount(), 0u);
+
+  base::TimeDelta upload_interval = client_.GetUploadInterval();
+  task_environment_.FastForwardBy(upload_interval);
+
+  EXPECT_EQ(GetUnsentLogCount(), 1u);
+
+  // Simulate logs upload.
+  client_.uploader()->CompleteUpload(200);
+
+  EXPECT_EQ(GetUnsentLogCount(), 0u);
+
+  base::PumaHistogramBoolean(base::PumaType::kRc,
+                             "PUMA.PumaServiceTestHistogram.Boolean12", true);
+
+  task_environment_.FastForwardBy(upload_interval);
+
+  EXPECT_EQ(GetUnsentLogCount(), 1u);
+
+  // Simulate logs upload.
+  client_.uploader()->CompleteUpload(200);
+
+  EXPECT_EQ(GetUnsentLogCount(), 0u);
+}
+
+}  // namespace metrics::private_metrics