[go: nahoru, domu]

Add test-only simulator for event-level Attribution Report API

We will address the following in followup CLs:

- Simulation mode: production, debug, seeded
- Including dropped sources, triggers, and reports in JSON output

Change-Id: I5283a55e10eb8e443093d9ffadc3d1d2d624b227
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/3421416
Reviewed-by: Dirk Pranke <dpranke@google.com>
Reviewed-by: Charlie Harrison <csharrison@chromium.org>
Reviewed-by: Jochen Eisinger <jochen@chromium.org>
Commit-Queue: Andrew Paseltiner <apaseltiner@chromium.org>
Cr-Commit-Position: refs/heads/main@{#966961}
diff --git a/BUILD.gn b/BUILD.gn
index f005ced..0f945b9 100644
--- a/BUILD.gn
+++ b/BUILD.gn
@@ -169,6 +169,7 @@
       "//ppapi/examples/video_encode",
       "//third_party/vulkan-deps/spirv-tools/src:SPIRV-Tools",
       "//tools/aggregation_service:aggregation_service_tool",
+      "//tools/attribution_reporting:attribution_reporting_simulator",
       "//tools/perf/clear_system_cache",
       "//tools/polymer:polymer_tools_python_unittests",
       "//tools/privacy_budget:privacy_budget_tools",
diff --git a/content/browser/attribution_reporting/attribution_internals_browsertest.cc b/content/browser/attribution_reporting/attribution_internals_browsertest.cc
index 1ea5993..545992be 100644
--- a/content/browser/attribution_reporting/attribution_internals_browsertest.cc
+++ b/content/browser/attribution_reporting/attribution_internals_browsertest.cc
@@ -69,7 +69,7 @@
     ON_CALL(manager_, GetActiveSourcesForWebUI)
         .WillByDefault(InvokeCallback<std::vector<StoredSource>>({}));
 
-    ON_CALL(manager_, GetPendingReportsForWebUI)
+    ON_CALL(manager_, GetPendingReportsForInternalUse)
         .WillByDefault(InvokeCallback<std::vector<AttributionReport>>({}));
   }
 
@@ -358,7 +358,7 @@
                                 .Build(),
                             SendResult(SendResult::Status::kFailure,
                                        /*http_response_code=*/0));
-  ON_CALL(manager_, GetPendingReportsForWebUI)
+  ON_CALL(manager_, GetPendingReportsForInternalUse)
       .WillByDefault(InvokeCallback<std::vector<AttributionReport>>(
           {ReportBuilder(
                SourceBuilder(now)
@@ -502,7 +502,7 @@
                                  .SetReportTime(now)
                                  .SetPriority(7)
                                  .Build();
-  EXPECT_CALL(manager_, GetPendingReportsForWebUI)
+  EXPECT_CALL(manager_, GetPendingReportsForInternalUse)
       .WillOnce(InvokeCallback<std::vector<AttributionReport>>({report}));
 
   report.set_report_time(report.report_time() + base::Hours(1));
@@ -605,7 +605,7 @@
                        WebUISendReports_ReportsRemoved) {
   EXPECT_TRUE(NavigateToURL(shell(), GURL(kAttributionInternalsUrl)));
 
-  EXPECT_CALL(manager_, GetPendingReportsForWebUI)
+  EXPECT_CALL(manager_, GetPendingReportsForInternalUse)
       .WillOnce(InvokeCallback<std::vector<AttributionReport>>(
           {ReportBuilder(SourceBuilder().BuildStored())
                .SetPriority(7)
diff --git a/content/browser/attribution_reporting/attribution_internals_handler_impl.cc b/content/browser/attribution_reporting/attribution_internals_handler_impl.cc
index 7b8aba6..141c49cc 100644
--- a/content/browser/attribution_reporting/attribution_internals_handler_impl.cc
+++ b/content/browser/attribution_reporting/attribution_internals_handler_impl.cc
@@ -15,6 +15,7 @@
 #include "content/browser/attribution_reporting/attribution_manager_impl.h"
 #include "content/browser/attribution_reporting/attribution_report.h"
 #include "content/browser/attribution_reporting/attribution_storage.h"
+#include "content/browser/attribution_reporting/attribution_utils.h"
 #include "content/browser/attribution_reporting/common_source_info.h"
 #include "content/browser/attribution_reporting/send_result.h"
 #include "content/browser/attribution_reporting/stored_source.h"
@@ -95,7 +96,7 @@
       report.ReportURL(),
       /*trigger_time=*/report.trigger_time().ToJsTime(),
       /*report_time=*/report.report_time().ToJsTime(), data->priority,
-      report.ReportBody(/*pretty_print=*/true),
+      SerializeAttributionJson(report.ReportBody(), /*pretty_print=*/true),
       /*attributed_truthfully=*/
       report.source().attribution_logic() ==
           StoredSource::AttributionLogic::kTruthfully,
@@ -158,7 +159,7 @@
     mojom::AttributionInternalsHandler::GetReportsCallback callback) {
   if (AttributionManager* manager =
           manager_provider_->GetManager(web_ui_->GetWebContents())) {
-    manager->GetPendingReportsForWebUI(
+    manager->GetPendingReportsForInternalUse(
         base::BindOnce(&ForwardReportsToWebUI, std::move(callback)));
   } else {
     std::move(callback).Run({});
diff --git a/content/browser/attribution_reporting/attribution_manager.h b/content/browser/attribution_reporting/attribution_manager.h
index 75f43b0..bd7763d 100644
--- a/content/browser/attribution_reporting/attribution_manager.h
+++ b/content/browser/attribution_reporting/attribution_manager.h
@@ -85,8 +85,8 @@
       base::OnceCallback<void(std::vector<StoredSource>)> callback) = 0;
 
   // Get all pending reports that are currently stored in this partition. Used
-  // for populating WebUI.
-  virtual void GetPendingReportsForWebUI(
+  // for populating WebUI and simulator.
+  virtual void GetPendingReportsForInternalUse(
       base::OnceCallback<void(std::vector<AttributionReport>)> callback) = 0;
 
   // Sends the given reports immediately, and runs |done| once they have all
diff --git a/content/browser/attribution_reporting/attribution_manager_impl.cc b/content/browser/attribution_reporting/attribution_manager_impl.cc
index 08c6539a..c1182ca 100644
--- a/content/browser/attribution_reporting/attribution_manager_impl.cc
+++ b/content/browser/attribution_reporting/attribution_manager_impl.cc
@@ -15,6 +15,7 @@
 #include "base/task/lazy_thread_pool_task_runner.h"
 #include "base/threading/sequence_bound.h"
 #include "base/time/time.h"
+#include "base/values.h"
 #include "content/browser/attribution_reporting/attribution_network_sender_impl.h"
 #include "content/browser/attribution_reporting/attribution_policy.h"
 #include "content/browser/attribution_reporting/attribution_report.h"
@@ -137,12 +138,45 @@
   AttributionStorageSql::RunInMemoryForTesting();
 }
 
+// static
+AttributionManagerImpl::IsReportAllowedCallback
+AttributionManagerImpl::DefaultIsReportAllowedCallback(
+    BrowserContext* browser_context) {
+  return base::BindRepeating(
+      [](BrowserContext* browser_context, const AttributionReport& report) {
+        const CommonSourceInfo& common_info = report.source().common_info();
+        return GetContentClient()
+            ->browser()
+            ->IsConversionMeasurementOperationAllowed(
+                browser_context,
+                ContentBrowserClient::ConversionMeasurementOperation::kReport,
+                &common_info.impression_origin(),
+                &common_info.conversion_origin(),
+                &common_info.reporting_origin());
+      },
+      browser_context);
+}
+
+// static
+std::unique_ptr<AttributionManagerImpl>
+AttributionManagerImpl::CreateForTesting(
+    IsReportAllowedCallback is_report_allowed_callback,
+    const base::FilePath& user_data_directory,
+    scoped_refptr<storage::SpecialStoragePolicy> special_storage_policy,
+    std::unique_ptr<AttributionStorage::Delegate> storage_delegate,
+    std::unique_ptr<NetworkSender> network_sender) {
+  return absl::WrapUnique(new AttributionManagerImpl(
+      std::move(is_report_allowed_callback), user_data_directory,
+      std::move(special_storage_policy), std::move(storage_delegate),
+      std::move(network_sender)));
+}
+
 AttributionManagerImpl::AttributionManagerImpl(
     StoragePartitionImpl* storage_partition,
     const base::FilePath& user_data_directory,
     scoped_refptr<storage::SpecialStoragePolicy> special_storage_policy)
     : AttributionManagerImpl(
-          storage_partition,
+          DefaultIsReportAllowedCallback(storage_partition->browser_context()),
           user_data_directory,
           std::move(special_storage_policy),
           std::make_unique<AttributionStorageDelegateImpl>(
@@ -151,12 +185,12 @@
           std::make_unique<AttributionNetworkSenderImpl>(storage_partition)) {}
 
 AttributionManagerImpl::AttributionManagerImpl(
-    StoragePartitionImpl* storage_partition,
+    IsReportAllowedCallback is_report_allowed_callback,
     const base::FilePath& user_data_directory,
     scoped_refptr<storage::SpecialStoragePolicy> special_storage_policy,
     std::unique_ptr<AttributionStorage::Delegate> storage_delegate,
     std::unique_ptr<NetworkSender> network_sender)
-    : storage_partition_(storage_partition),
+    : is_report_allowed_callback_(std::move(is_report_allowed_callback)),
       attribution_storage_(base::SequenceBound<AttributionStorageSql>(
           g_storage_task_runner.Get(),
           user_data_directory,
@@ -164,7 +198,7 @@
       special_storage_policy_(std::move(special_storage_policy)),
       network_sender_(std::move(network_sender)),
       weak_factory_(this) {
-  DCHECK(storage_partition_);
+  DCHECK(is_report_allowed_callback_);
   DCHECK(network_sender_);
 
   content::GetNetworkConnectionTracker()->AddNetworkConnectionObserver(this);
@@ -234,6 +268,11 @@
           weak_factory_.GetWeakPtr()));
 }
 
+void AttributionManagerImpl::HandleSourceInternalForTesting(
+    StorableSource source) {
+  HandleSourceInternal(std::move(source));
+}
+
 void AttributionManagerImpl::HandleTrigger(AttributionTrigger trigger) {
   GetContentClient()->browser()->FlushBackgroundAttributions(
       base::BindOnce(&AttributionManagerImpl::HandleTriggerInternal,
@@ -247,6 +286,11 @@
                            weak_factory_.GetWeakPtr()));
 }
 
+void AttributionManagerImpl::HandleTriggerInternalForTesting(
+    AttributionTrigger trigger) {
+  HandleTriggerInternal(std::move(trigger));
+}
+
 void AttributionManagerImpl::OnReportStored(CreateReportResult result) {
   RecordCreateReportStatus(result.status());
 
@@ -279,7 +323,7 @@
       .Then(std::move(callback));
 }
 
-void AttributionManagerImpl::GetPendingReportsForWebUI(
+void AttributionManagerImpl::GetPendingReportsForInternalUse(
     base::OnceCallback<void(std::vector<AttributionReport>)> callback) {
   GetAndHandleReports(std::move(callback),
                       /*max_report_time=*/base::Time::Max(), /*limit=*/1000);
@@ -428,14 +472,7 @@
       continue;
     }
 
-    bool allowed =
-        GetContentClient()->browser()->IsConversionMeasurementOperationAllowed(
-            storage_partition_->browser_context(),
-            ContentBrowserClient::ConversionMeasurementOperation::kReport,
-            &report.source().common_info().impression_origin(),
-            &report.source().common_info().conversion_origin(),
-            &report.source().common_info().reporting_origin());
-    if (!allowed) {
+    if (!is_report_allowed_callback_.Run(report)) {
       // If measurement is disallowed, just drop the report on the floor. We
       // need to make sure we forward that the report was "sent" to ensure it is
       // deleted from storage, etc. This simulates sending the report through a
@@ -450,7 +487,7 @@
       LogMetricsOnReportSend(report, now);
 
     GURL report_url = report.ReportURL();
-    std::string report_body = report.ReportBody();
+    base::Value report_body = report.ReportBody();
     network_sender_->SendReport(
         std::move(report_url), std::move(report_body),
         base::BindOnce(&AttributionManagerImpl::OnReportSent,
diff --git a/content/browser/attribution_reporting/attribution_manager_impl.h b/content/browser/attribution_reporting/attribution_manager_impl.h
index c5ef3dc..eccf1a4b 100644
--- a/content/browser/attribution_reporting/attribution_manager_impl.h
+++ b/content/browser/attribution_reporting/attribution_manager_impl.h
@@ -6,7 +6,6 @@
 #define CONTENT_BROWSER_ATTRIBUTION_REPORTING_ATTRIBUTION_MANAGER_IMPL_H_
 
 #include <memory>
-#include <string>
 #include <vector>
 
 #include "base/callback_forward.h"
@@ -29,10 +28,12 @@
 
 namespace base {
 class FilePath;
+class Value;
 }  // namespace base
 
 namespace content {
 
+class BrowserContext;
 class StoragePartitionImpl;
 
 struct SendResult;
@@ -83,14 +84,27 @@
     // Generates and sends a conversion report matching |report|. This should
     // generate a secure POST request with no-credentials.
     virtual void SendReport(GURL report_url,
-                            std::string report_body,
+                            base::Value report_body,
                             ReportSentCallback sent_callback) = 0;
   };
 
+  using IsReportAllowedCallback =
+      base::RepeatingCallback<bool(const AttributionReport&)>;
+
+  static IsReportAllowedCallback DefaultIsReportAllowedCallback(
+      BrowserContext*);
+
   // Configures underlying storage to be setup in memory, rather than on
   // disk. This speeds up initialization to avoid timeouts in test environments.
   static void RunInMemoryForTesting();
 
+  static std::unique_ptr<AttributionManagerImpl> CreateForTesting(
+      IsReportAllowedCallback is_report_allowed_callback,
+      const base::FilePath& user_data_directory,
+      scoped_refptr<storage::SpecialStoragePolicy> special_storage_policy,
+      std::unique_ptr<AttributionStorage::Delegate> storage_delegate,
+      std::unique_ptr<NetworkSender> network_sender);
+
   AttributionManagerImpl(
       StoragePartitionImpl* storage_partition,
       const base::FilePath& user_data_directory,
@@ -109,7 +123,7 @@
   void HandleTrigger(AttributionTrigger trigger) override;
   void GetActiveSourcesForWebUI(
       base::OnceCallback<void(std::vector<StoredSource>)> callback) override;
-  void GetPendingReportsForWebUI(
+  void GetPendingReportsForInternalUse(
       base::OnceCallback<void(std::vector<AttributionReport>)> callback)
       override;
   void SendReportsForWebUI(
@@ -120,11 +134,14 @@
                  base::RepeatingCallback<bool(const url::Origin&)> filter,
                  base::OnceClosure done) override;
 
+  void HandleSourceInternalForTesting(StorableSource);
+  void HandleTriggerInternalForTesting(AttributionTrigger);
+
  private:
   friend class AttributionManagerImplTest;
 
   AttributionManagerImpl(
-      StoragePartitionImpl* storage_partition,
+      IsReportAllowedCallback is_report_allowed_callback,
       const base::FilePath& user_data_directory,
       scoped_refptr<storage::SpecialStoragePolicy> special_storage_policy,
       std::unique_ptr<AttributionStorage::Delegate> storage_delegate,
@@ -174,7 +191,8 @@
       AttributionManagerImpl* manager,
       base::Time max_report_time);
 
-  raw_ptr<StoragePartitionImpl> storage_partition_;
+  // Internally holds a non-owning pointer to `BrowserContext`.
+  IsReportAllowedCallback is_report_allowed_callback_;
 
   base::SequenceBound<AttributionStorage> attribution_storage_;
 
diff --git a/content/browser/attribution_reporting/attribution_manager_impl_unittest.cc b/content/browser/attribution_reporting/attribution_manager_impl_unittest.cc
index 7fa3067..8bc76c7 100644
--- a/content/browser/attribution_reporting/attribution_manager_impl_unittest.cc
+++ b/content/browser/attribution_reporting/attribution_manager_impl_unittest.cc
@@ -23,6 +23,7 @@
 #include "base/test/metrics/histogram_tester.h"
 #include "base/test/mock_callback.h"
 #include "base/time/time.h"
+#include "base/values.h"
 #include "build/build_config.h"
 #include "content/browser/attribution_reporting/attribution_report.h"
 #include "content/browser/attribution_reporting/attribution_storage.h"
@@ -33,7 +34,6 @@
 #include "content/browser/attribution_reporting/send_result.h"
 #include "content/browser/attribution_reporting/storable_source.h"
 #include "content/browser/attribution_reporting/stored_source.h"
-#include "content/browser/storage_partition_impl.h"
 #include "content/public/browser/browser_context.h"
 #include "content/public/browser/network_service_instance.h"
 #include "content/public/test/browser_task_environment.h"
@@ -116,13 +116,13 @@
  public:
   // AttributionManagerImpl::NetworkSender:
   void SendReport(GURL report_url,
-                  std::string report_body,
+                  base::Value report_body,
                   ReportSentCallback callback) override {
     calls_.emplace_back(std::move(report_url), std::move(report_body));
     callbacks_.push_back(std::move(callback));
   }
 
-  using SendReportCalls = std::vector<std::pair<GURL, std::string>>;
+  using SendReportCalls = std::vector<std::pair<GURL, base::Value>>;
 
   const SendReportCalls& calls() const { return calls_; }
 
@@ -174,12 +174,12 @@
   }
 
   void CreateManager() {
-    attribution_manager_ = absl::WrapUnique(new AttributionManagerImpl(
-        static_cast<StoragePartitionImpl*>(
-            browser_context_->GetDefaultStoragePartition()),
+    attribution_manager_ = AttributionManagerImpl::CreateForTesting(
+        AttributionManagerImpl::DefaultIsReportAllowedCallback(
+            browser_context_.get()),
         dir_.GetPath(), mock_storage_policy_,
         std::make_unique<NoRandomizedResponseStorageDelegate>(),
-        absl::WrapUnique(network_sender_.get())));
+        absl::WrapUnique(network_sender_.get()));
   }
 
   void ShutdownManager() {
@@ -206,7 +206,7 @@
   std::vector<AttributionReport> StoredReports() {
     std::vector<AttributionReport> result;
     base::RunLoop loop;
-    attribution_manager_->GetPendingReportsForWebUI(
+    attribution_manager_->GetPendingReportsForInternalUse(
         base::BindLambdaForTesting([&](std::vector<AttributionReport> reports) {
           result = std::move(reports);
           loop.Quit();
diff --git a/content/browser/attribution_reporting/attribution_network_sender_impl.cc b/content/browser/attribution_reporting/attribution_network_sender_impl.cc
index d8ed1983..23e94b0 100644
--- a/content/browser/attribution_reporting/attribution_network_sender_impl.cc
+++ b/content/browser/attribution_reporting/attribution_network_sender_impl.cc
@@ -10,6 +10,7 @@
 #include "base/bind.h"
 #include "base/check.h"
 #include "base/metrics/histogram_functions.h"
+#include "content/browser/attribution_reporting/attribution_utils.h"
 #include "content/browser/attribution_reporting/send_result.h"
 #include "content/public/browser/storage_partition.h"
 #include "net/base/isolation_info.h"
@@ -49,7 +50,7 @@
 
 void AttributionNetworkSenderImpl::SendReport(
     GURL report_url,
-    std::string report_body,
+    base::Value report_body,
     ReportSentCallback sent_callback) {
   // The browser process URLLoaderFactory is not created by default, so don't
   // create it until it is directly needed.
@@ -103,7 +104,8 @@
                                         std::move(simple_url_loader));
   simple_url_loader_ptr->SetTimeoutDuration(base::Seconds(30));
 
-  simple_url_loader_ptr->AttachStringForUpload(report_body, "application/json");
+  simple_url_loader_ptr->AttachStringForUpload(
+      SerializeAttributionJson(report_body), "application/json");
 
   // Retry once on network change. A network change during DNS resolution
   // results in a DNS error rather than a network change error, so retry in
diff --git a/content/browser/attribution_reporting/attribution_network_sender_impl.h b/content/browser/attribution_reporting/attribution_network_sender_impl.h
index f78186d..8aa80c5 100644
--- a/content/browser/attribution_reporting/attribution_network_sender_impl.h
+++ b/content/browser/attribution_reporting/attribution_network_sender_impl.h
@@ -47,7 +47,7 @@
   // |sent_callback| is run after the request finishes, whether or not it
   // succeeded,
   void SendReport(GURL report_url,
-                  std::string report_body,
+                  base::Value report_body,
                   ReportSentCallback sent_callback) override;
 
   // Tests inject a TestURLLoaderFactory so they can mock the network response.
diff --git a/content/browser/attribution_reporting/attribution_report.cc b/content/browser/attribution_reporting/attribution_report.cc
index e4965b6..5f1f54f 100644
--- a/content/browser/attribution_reporting/attribution_report.cc
+++ b/content/browser/attribution_reporting/attribution_report.cc
@@ -7,7 +7,6 @@
 #include <utility>
 
 #include "base/check.h"
-#include "base/json/json_writer.h"
 #include "base/strings/string_number_conversions.h"
 #include "base/values.h"
 #include "content/browser/attribution_reporting/attribution_utils.h"
@@ -108,7 +107,7 @@
       replacements);
 }
 
-std::string AttributionReport::ReportBody(bool pretty_print) const {
+base::Value AttributionReport::ReportBody() const {
   const auto* event_data = absl::get_if<EventLevelData>(&data_);
   DCHECK(event_data);
 
@@ -152,13 +151,7 @@
   dict.SetDoubleKey("randomized_trigger_rate",
                     RandomizedTriggerRate(source_.common_info().source_type()));
 
-  // Write the dict to json;
-  std::string output_json;
-  bool success = base::JSONWriter::WriteWithOptions(
-      dict, pretty_print ? base::JSONWriter::OPTIONS_PRETTY_PRINT : 0,
-      &output_json);
-  DCHECK(success);
-  return output_json;
+  return dict;
 }
 
 absl::optional<AttributionReport::Id> AttributionReport::ReportId() const {
diff --git a/content/browser/attribution_reporting/attribution_report.h b/content/browser/attribution_reporting/attribution_report.h
index bd5f1ed8..1b566cd5 100644
--- a/content/browser/attribution_reporting/attribution_report.h
+++ b/content/browser/attribution_reporting/attribution_report.h
@@ -7,8 +7,6 @@
 
 #include <stdint.h>
 
-#include <string>
-
 #include "base/guid.h"
 #include "base/time/time.h"
 #include "base/types/strong_alias.h"
@@ -20,6 +18,10 @@
 
 class GURL;
 
+namespace base {
+class Value;
+}  // namespace base
+
 namespace content {
 
 // Class that contains all the data needed to serialize and send an attribution
@@ -98,8 +100,7 @@
   // Returns the URL to which the report will be sent.
   GURL ReportURL() const;
 
-  // Returns the JSON for the report body.
-  std::string ReportBody(bool pretty_print = false) const;
+  base::Value ReportBody() const;
 
   absl::optional<Id> ReportId() const;
 
diff --git a/content/browser/attribution_reporting/attribution_test_utils.h b/content/browser/attribution_reporting/attribution_test_utils.h
index fc94c59..59307c6 100644
--- a/content/browser/attribution_reporting/attribution_test_utils.h
+++ b/content/browser/attribution_reporting/attribution_test_utils.h
@@ -201,7 +201,7 @@
 
   MOCK_METHOD(
       void,
-      GetPendingReportsForWebUI,
+      GetPendingReportsForInternalUse,
       (base::OnceCallback<void(std::vector<AttributionReport>)> callback),
       (override));
 
diff --git a/content/browser/attribution_reporting/attribution_utils.cc b/content/browser/attribution_reporting/attribution_utils.cc
index 95bfdb4a..17a0306 100644
--- a/content/browser/attribution_reporting/attribution_utils.cc
+++ b/content/browser/attribution_reporting/attribution_utils.cc
@@ -7,6 +7,7 @@
 #include "base/check.h"
 #include "base/check_op.h"
 #include "base/containers/span.h"
+#include "base/json/json_writer.h"
 #include "base/time/time.h"
 
 namespace content {
@@ -129,4 +130,15 @@
   }
 }
 
+std::string SerializeAttributionJson(const base::Value& body,
+                                     bool pretty_print) {
+  int options = pretty_print ? base::JSONWriter::OPTIONS_PRETTY_PRINT : 0;
+
+  std::string output_json;
+  bool success =
+      base::JSONWriter::WriteWithOptions(body, options, &output_json);
+  DCHECK(success);
+  return output_json;
+}
+
 }  // namespace content
diff --git a/content/browser/attribution_reporting/attribution_utils.h b/content/browser/attribution_reporting/attribution_utils.h
index e95e73ff..8a517e2 100644
--- a/content/browser/attribution_reporting/attribution_utils.h
+++ b/content/browser/attribution_reporting/attribution_utils.h
@@ -7,10 +7,13 @@
 
 #include <stdint.h>
 
+#include <string>
+
 #include "content/browser/attribution_reporting/common_source_info.h"
 
 namespace base {
 class Time;
+class Value;
 }  // namespace base
 
 namespace content {
@@ -30,6 +33,9 @@
 
 double RandomizedTriggerRate(CommonSourceInfo::SourceType source_type);
 
+std::string SerializeAttributionJson(const base::Value& body,
+                                     bool pretty_print = false);
+
 }  // namespace content
 
 #endif  // CONTENT_BROWSER_ATTRIBUTION_REPORTING_ATTRIBUTION_UTILS_H_
diff --git a/content/public/test/OWNERS b/content/public/test/OWNERS
index 1dfe04a..6ca2394e 100644
--- a/content/public/test/OWNERS
+++ b/content/public/test/OWNERS
@@ -13,6 +13,9 @@
 # For FakeSpeechRecognitionManager, used by the a11y Dictation feature.
 per-file fake_speech_recognition_manager*=file://ui/accessibility/OWNERS
 
+# For Attribution Reporting API simulator.
+per-file attribution_simulator*=file://content/browser/attribution_reporting/OWNERS
+
 # For security presubmit checks, though test mojom really don't need review.
 per-file *.mojom=set noparent
 per-file *.mojom=file://ipc/SECURITY_OWNERS
diff --git a/content/public/test/attribution_simulator.cc b/content/public/test/attribution_simulator.cc
new file mode 100644
index 0000000..5903278e
--- /dev/null
+++ b/content/public/test/attribution_simulator.cc
@@ -0,0 +1,20 @@
+// Copyright 2022 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "content/public/test/attribution_simulator.h"
+
+#include "base/values.h"
+#include "content/test/attribution_simulator_impl.h"
+#include "content/test/attribution_simulator_input_parser.h"
+
+namespace content {
+
+base::Value RunAttributionSimulationOrExit(
+    const base::Value& input,
+    const AttributionSimulationOptions& options) {
+  return RunAttributionSimulation(ParseAttributionSimulationInputOrExit(input),
+                                  options);
+}
+
+}  // namespace content
diff --git a/content/public/test/attribution_simulator.h b/content/public/test/attribution_simulator.h
new file mode 100644
index 0000000..f0e6aac
--- /dev/null
+++ b/content/public/test/attribution_simulator.h
@@ -0,0 +1,34 @@
+// Copyright 2022 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef CONTENT_PUBLIC_TEST_ATTRIBUTION_SIMULATOR_H_
+#define CONTENT_PUBLIC_TEST_ATTRIBUTION_SIMULATOR_H_
+
+namespace base {
+class Value;
+}  // namespace base
+
+namespace content {
+
+struct AttributionSimulationOptions {
+  // If true, removes the `report_id` field from reports before output.
+  //
+  // This field normally contains a random GUID used by the reporting origin
+  // to deduplicate reports in the event of retries. As such, it is a source
+  // of nondeterminism in the output.
+  bool remove_report_ids = false;
+};
+
+// Simulates the Attribution Reporting API for a single user on sources and
+// triggers specified in `input`. Returns the generated reports, if any, as a
+// JSON document.
+//
+// Exits if `input` cannot be parsed.
+base::Value RunAttributionSimulationOrExit(
+    const base::Value& input,
+    const AttributionSimulationOptions& options);
+
+}  // namespace content
+
+#endif  // CONTENT_PUBLIC_TEST_ATTRIBUTION_SIMULATOR_H_
diff --git a/content/test/BUILD.gn b/content/test/BUILD.gn
index 93fd134..076ec7e 100644
--- a/content/test/BUILD.gn
+++ b/content/test/BUILD.gn
@@ -106,6 +106,8 @@
     "../browser/worker_host/test_shared_worker_service_impl.h",
     "../public/test/accessibility_notification_waiter.cc",
     "../public/test/accessibility_notification_waiter.h",
+    "../public/test/attribution_simulator.cc",
+    "../public/test/attribution_simulator.h",
     "../public/test/audio_service_test_helper.cc",
     "../public/test/audio_service_test_helper.h",
     "../public/test/back_forward_cache_util.cc",
@@ -287,6 +289,10 @@
     "../public/test/web_ui_browsertest_util.h",
     "../renderer/mock_agent_scheduling_group.cc",
     "../renderer/mock_agent_scheduling_group.h",
+    "attribution_simulator_impl.cc",
+    "attribution_simulator_impl.h",
+    "attribution_simulator_input_parser.cc",
+    "attribution_simulator_input_parser.h",
     "content_browser_consistency_checker.cc",
     "content_browser_consistency_checker.h",
     "content_test_suite.cc",
diff --git a/content/test/OWNERS b/content/test/OWNERS
index 822107e..756ac8cc 100644
--- a/content/test/OWNERS
+++ b/content/test/OWNERS
@@ -28,6 +28,9 @@
 # Aggregation service related files.
 per-file *aggregation_service*=file://content/browser/aggregation_service/OWNERS
 
+# For Attribution Reporting API simulator.
+per-file attribution_simulator*=file://content/browser/attribution_reporting/OWNERS
+
 # Anyone can add rules to include new test files.
 per-file BUILD.gn=*
 
diff --git a/content/test/attribution_simulator_impl.cc b/content/test/attribution_simulator_impl.cc
new file mode 100644
index 0000000..f94593de
--- /dev/null
+++ b/content/test/attribution_simulator_impl.cc
@@ -0,0 +1,169 @@
+// Copyright 2022 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "content/test/attribution_simulator_impl.h"
+
+#include <memory>
+#include <utility>
+
+#include "base/files/file_path.h"
+#include "base/memory/raw_ptr.h"
+#include "base/ranges/algorithm.h"
+#include "base/run_loop.h"
+#include "base/test/bind.h"
+#include "base/test/task_environment.h"
+#include "base/test/test_timeouts.h"
+#include "base/values.h"
+#include "content/browser/attribution_reporting/attribution_manager_impl.h"
+#include "content/browser/attribution_reporting/attribution_storage_delegate_impl.h"
+#include "content/browser/attribution_reporting/common_source_info.h"
+#include "content/browser/attribution_reporting/send_result.h"
+#include "content/public/browser/network_service_instance.h"
+#include "content/public/test/attribution_simulator.h"
+#include "content/public/test/browser_task_environment.h"
+#include "services/network/test/test_network_connection_tracker.h"
+#include "url/gurl.h"
+
+namespace content {
+
+namespace {
+
+base::Time GetEventTime(const AttributionSimulationEvent& event) {
+  struct Visitor {
+    base::Time operator()(const StorableSource& source) {
+      return source.common_info().impression_time();
+    }
+
+    base::Time operator()(const AttributionTriggerAndTime& trigger) {
+      return trigger.time;
+    }
+  };
+
+  return absl::visit(Visitor{}, event);
+}
+
+class SentReportAccumulator : public AttributionManagerImpl::NetworkSender {
+ public:
+  SentReportAccumulator(base::Value::ListStorage& reports,
+                        bool remove_report_ids)
+      : time_origin_(base::Time::Now()),
+        remove_report_ids_(remove_report_ids),
+        reports_(reports) {}
+
+  ~SentReportAccumulator() override = default;
+
+  SentReportAccumulator(const SentReportAccumulator&) = delete;
+  SentReportAccumulator(SentReportAccumulator&&) = delete;
+
+  SentReportAccumulator& operator=(const SentReportAccumulator&) = delete;
+  SentReportAccumulator& operator=(SentReportAccumulator&&) = delete;
+
+ private:
+  // AttributionManagerImpl::NetworkSender:
+  void SendReport(GURL report_url,
+                  base::Value report_body,
+                  ReportSentCallback sent_callback) override {
+    if (remove_report_ids_)
+      report_body.RemoveKey("report_id");
+
+    base::DictionaryValue value;
+    value.SetKey("report", std::move(report_body));
+    value.SetStringKey("report_url", report_url.spec());
+    value.SetIntKey("report_time",
+                    (base::Time::Now() - time_origin_).InSeconds());
+
+    reports_.push_back(std::move(value));
+
+    std::move(sent_callback)
+        .Run(SendResult(SendResult::Status::kSent,
+                        /*http_response_code=*/200));
+  }
+
+  const base::Time time_origin_;
+  const bool remove_report_ids_;
+  base::Value::ListStorage& reports_;
+};
+
+struct EventHandler {
+  base::raw_ptr<AttributionManagerImpl> manager;
+
+  void operator()(StorableSource source) {
+    manager->HandleSourceInternalForTesting(std::move(source));
+  }
+
+  void operator()(AttributionTriggerAndTime trigger) {
+    manager->HandleTriggerInternalForTesting(std::move(trigger.trigger));
+  }
+};
+
+}  // namespace
+
+base::Value RunAttributionSimulation(
+    std::vector<AttributionSimulationEvent> events,
+    const AttributionSimulationOptions& options) {
+  base::ranges::stable_sort(events, /*comp=*/{}, &GetEventTime);
+
+  // Prerequisites for using an environment with mock time.
+  TestTimeouts::Initialize();
+  content::BrowserTaskEnvironment task_environment(
+      base::test::TaskEnvironment::TimeSource::MOCK_TIME);
+
+  // Avoid creating an on-disk sqlite DB.
+  content::AttributionManagerImpl::RunInMemoryForTesting();
+
+  // Ensure that the manager always thinks the browser is online.
+  auto network_connection_tracker =
+      network::TestNetworkConnectionTracker::CreateInstance();
+  content::SetNetworkConnectionTrackerForTesting(
+      network_connection_tracker.get());
+
+  auto always_allow_reports_callback =
+      base::BindRepeating([](const AttributionReport&) { return true; });
+
+  // This isn't needed because the DB is completely in memory for testing.
+  const base::FilePath user_data_directory;
+
+  base::Value::ListStorage reports;
+  auto manager = AttributionManagerImpl::CreateForTesting(
+      std::move(always_allow_reports_callback), user_data_directory,
+      /*special_storage_policy=*/nullptr,
+      std::make_unique<AttributionStorageDelegateImpl>(),
+      /*network_sender=*/
+      std::make_unique<SentReportAccumulator>(reports,
+                                              options.remove_report_ids));
+
+  // TODO(apaseltiner): Add an `AttributionManager::Observer` to `manager` so we
+  // can record dropped reports in the output.
+
+  EventHandler handler{.manager = manager.get()};
+
+  for (auto& event : events) {
+    task_environment.FastForwardBy(GetEventTime(event) - base::Time::Now());
+    absl::visit(handler, std::move(event));
+  }
+
+  absl::optional<base::Time> last_report_time;
+
+  base::RunLoop loop;
+  manager->GetPendingReportsForInternalUse(
+      base::BindLambdaForTesting([&](std::vector<AttributionReport> reports) {
+        if (!reports.empty()) {
+          last_report_time = base::ranges::max(reports, /*comp=*/{},
+                                               &AttributionReport::report_time)
+                                 .report_time();
+        }
+
+        loop.Quit();
+      }));
+
+  loop.Run();
+  if (last_report_time.has_value())
+    task_environment.FastForwardBy(*last_report_time - base::Time::Now());
+
+  base::DictionaryValue output;
+  output.SetKey("reports", base::Value(std::move(reports)));
+  return output;
+}
+
+}  // namespace content
diff --git a/content/test/attribution_simulator_impl.h b/content/test/attribution_simulator_impl.h
new file mode 100644
index 0000000..27602bdd
--- /dev/null
+++ b/content/test/attribution_simulator_impl.h
@@ -0,0 +1,32 @@
+// Copyright 2022 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef CONTENT_TEST_ATTRIBUTION_SIMULATOR_IMPL_H_
+#define CONTENT_TEST_ATTRIBUTION_SIMULATOR_IMPL_H_
+
+#include <vector>
+
+#include "base/time/time.h"
+#include "content/browser/attribution_reporting/attribution_trigger.h"
+#include "content/browser/attribution_reporting/storable_source.h"
+#include "content/public/test/attribution_simulator.h"
+#include "third_party/abseil-cpp/absl/types/variant.h"
+
+namespace content {
+
+struct AttributionTriggerAndTime {
+  AttributionTrigger trigger;
+  base::Time time;
+};
+
+using AttributionSimulationEvent =
+    absl::variant<StorableSource, AttributionTriggerAndTime>;
+
+base::Value RunAttributionSimulation(
+    std::vector<AttributionSimulationEvent> events,
+    const AttributionSimulationOptions& options);
+
+}  // namespace content
+
+#endif  // CONTENT_TEST_ATTRIBUTION_SIMULATOR_IMPL_H_
diff --git a/content/test/attribution_simulator_input_parser.cc b/content/test/attribution_simulator_input_parser.cc
new file mode 100644
index 0000000..1f398ad
--- /dev/null
+++ b/content/test/attribution_simulator_input_parser.cc
@@ -0,0 +1,173 @@
+// Copyright 2022 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "content/test/attribution_simulator_input_parser.h"
+
+#include <stdint.h>
+
+#include <string>
+#include <utility>
+
+#include "base/logging.h"
+#include "base/strings/string_number_conversions.h"
+#include "base/time/time.h"
+#include "base/values.h"
+#include "content/browser/attribution_reporting/attribution_policy.h"
+#include "content/browser/attribution_reporting/attribution_trigger.h"
+#include "content/browser/attribution_reporting/common_source_info.h"
+#include "content/browser/attribution_reporting/storable_source.h"
+#include "net/base/schemeful_site.h"
+#include "url/gurl.h"
+#include "url/origin.h"
+
+namespace content {
+
+namespace {
+
+std::string FindStringKeyOrExit(const base::Value& dict, const char* key) {
+  const std::string* v = dict.FindStringKey(key);
+  LOG_IF(FATAL, !v) << "key not found: " << key;
+  return *v;
+}
+
+url::Origin FindOriginKeyOrExit(const base::Value& dict, const char* key) {
+  std::string v = FindStringKeyOrExit(dict, key);
+  auto origin = url::Origin::Create(GURL(v));
+  LOG_IF(FATAL, origin.opaque()) << "opaque origin: " << v;
+  return origin;
+}
+
+base::Time FindTimeKeyOrExit(const base::Value& dict, const char* key) {
+  absl::optional<int> v = dict.FindIntKey(key);
+  LOG_IF(FATAL, !v) << "key not found: " << key;
+  LOG_IF(FATAL, *v < 0) << "negative time not allowed: " << *v;
+  return base::Time::Now() + base::Seconds(*v);
+}
+
+uint64_t ParseUint64OrExit(const std::string& s) {
+  uint64_t v = 0;
+  LOG_IF(FATAL, !base::StringToUint64(s, &v)) << "invalid uint64: " << s;
+  return v;
+}
+
+int64_t ParseInt64OrExit(const std::string& s) {
+  int64_t v = 0;
+  LOG_IF(FATAL, !base::StringToInt64(s, &v)) << "invalid int64: " << s;
+  return v;
+}
+
+uint64_t FindUint64KeyOrDefault(const base::Value& dict,
+                                const char* key,
+                                uint64_t default_val) {
+  const std::string* s = dict.FindStringKey(key);
+  if (!s)
+    return default_val;
+
+  return ParseUint64OrExit(*s);
+}
+
+int64_t FindInt64KeyOrDefault(const base::Value& dict,
+                              const char* key,
+                              int64_t default_val) {
+  const std::string* s = dict.FindStringKey(key);
+  if (!s)
+    return default_val;
+
+  return ParseInt64OrExit(*s);
+}
+
+absl::optional<int64_t> FindInt64KeyOrNull(const base::Value& dict,
+                                           const char* key) {
+  const std::string* s = dict.FindStringKey(key);
+  if (!s)
+    return absl::nullopt;
+
+  return ParseInt64OrExit(*s);
+}
+
+uint64_t FindUint64KeyOrExit(const base::Value& dict, const char* key) {
+  return ParseUint64OrExit(FindStringKeyOrExit(dict, key));
+}
+
+CommonSourceInfo::SourceType FindSourceTypeKeyOrExit(const base::Value& dict,
+                                                     const char* key) {
+  std::string v = FindStringKeyOrExit(dict, key);
+
+  if (v == "navigation")
+    return CommonSourceInfo::SourceType::kNavigation;
+
+  if (v == "event")
+    return CommonSourceInfo::SourceType::kEvent;
+
+  LOG(FATAL) << "invalid source type: " << v;
+  return CommonSourceInfo::SourceType::kNavigation;
+}
+
+const base::Value& FindValueOrExit(const base::Value& dict, const char* key) {
+  const base::Value* v = dict.FindKey(key);
+  LOG_IF(FATAL, !v) << "key not found: " << key;
+  return *v;
+}
+
+StorableSource ParseSource(const base::Value& dict) {
+  const base::Value& cfg = FindValueOrExit(dict, "registration_config");
+
+  base::Time source_time = FindTimeKeyOrExit(dict, "source_time");
+
+  base::TimeDelta expiry = base::Days(30);
+  if (absl::optional<int64_t> v = FindInt64KeyOrNull(cfg, "expiry")) {
+    LOG_IF(FATAL, *v < 0) << "expiry must be >= 0: " << *v;
+    expiry = base::Milliseconds(*v);
+  }
+
+  CommonSourceInfo::SourceType source_type =
+      FindSourceTypeKeyOrExit(dict, "source_type");
+
+  return StorableSource(CommonSourceInfo(
+      FindUint64KeyOrExit(cfg, "source_event_id"),
+      FindOriginKeyOrExit(dict, "source_origin"),
+      FindOriginKeyOrExit(cfg, "destination"),
+      FindOriginKeyOrExit(dict, "reporting_origin"), source_time,
+      GetExpiryTimeForImpression(expiry, source_time, source_type), source_type,
+      FindInt64KeyOrDefault(cfg, "priority", 0)));
+}
+
+AttributionTriggerAndTime ParseTrigger(const base::Value& dict) {
+  const base::Value& cfg = FindValueOrExit(dict, "registration_config");
+
+  return AttributionTriggerAndTime{
+      .trigger = AttributionTrigger(
+          SanitizeTriggerData(FindUint64KeyOrDefault(cfg, "trigger_data", 0),
+                              CommonSourceInfo::SourceType::kNavigation),
+          net::SchemefulSite(FindOriginKeyOrExit(dict, "destination")),
+          FindOriginKeyOrExit(dict, "reporting_origin"),
+          SanitizeTriggerData(
+              FindUint64KeyOrDefault(cfg, "event_source_trigger_data", 0),
+              CommonSourceInfo::SourceType::kEvent),
+          FindInt64KeyOrDefault(cfg, "priority", 0),
+          FindInt64KeyOrNull(cfg, "dedup_key")),
+      .time = FindTimeKeyOrExit(dict, "trigger_time"),
+  };
+}
+
+}  // namespace
+
+std::vector<AttributionSimulationEvent> ParseAttributionSimulationInputOrExit(
+    const base::Value& input) {
+  std::vector<AttributionSimulationEvent> events;
+
+  if (const base::Value* items = input.FindListKey("sources")) {
+    base::ranges::transform(items->GetList(), std::back_inserter(events),
+                            &ParseSource);
+  }
+
+  if (const base::Value* items = input.FindListKey("triggers")) {
+    base::ranges::transform(items->GetList(), std::back_inserter(events),
+                            &ParseTrigger);
+  }
+
+  return events;
+}
+
+}  // namespace content
diff --git a/content/test/attribution_simulator_input_parser.h b/content/test/attribution_simulator_input_parser.h
new file mode 100644
index 0000000..9873580
--- /dev/null
+++ b/content/test/attribution_simulator_input_parser.h
@@ -0,0 +1,23 @@
+// Copyright 2022 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef CONTENT_TEST_ATTRIBUTION_SIMULATOR_INPUT_PARSER_H_
+#define CONTENT_TEST_ATTRIBUTION_SIMULATOR_INPUT_PARSER_H_
+
+#include <vector>
+
+#include "content/test/attribution_simulator_impl.h"
+
+namespace base {
+class Value;
+}  // namespace base
+
+namespace content {
+
+std::vector<AttributionSimulationEvent> ParseAttributionSimulationInputOrExit(
+    const base::Value& input);
+
+}  // namespace content
+
+#endif  // CONTENT_TEST_ATTRIBUTION_SIMULATOR_INPUT_PARSER_H_
diff --git a/content/test/data/attribution_reporting/simulator/input.json b/content/test/data/attribution_reporting/simulator/input.json
new file mode 100644
index 0000000..c3e028af
--- /dev/null
+++ b/content/test/data/attribution_reporting/simulator/input.json
@@ -0,0 +1,35 @@
+{
+  "sources": [
+    {
+      "source_type": "navigation",
+      "source_time": 1643235573,
+      "reporting_origin": "https://report.example",
+      "source_origin": "https://source.example",
+      "registration_config": {
+        "source_event_id": "1337",
+        "destination": "https://destination.example",
+        "expiry" : "864000000"
+      }
+    }
+  ],
+  "triggers": [
+    {
+      "trigger_time": 1643235574,
+      "reporting_origin": "https://report.example",
+      "destination": " https://destination.example",
+      "registration_config": {
+        "trigger_data": "3",
+        "event_source_trigger_data": "1"
+      }
+    },
+    {
+      "trigger_time": 1643235579,
+      "reporting_origin": "https://report.example",
+      "destination": "https://destination.example",
+      "registration_config": {
+        "trigger_data": "4",
+        "event_source_trigger_data": "0"
+      }
+    }
+  ]
+}
diff --git a/tools/attribution_reporting/BUILD.gn b/tools/attribution_reporting/BUILD.gn
new file mode 100644
index 0000000..1850b9e
--- /dev/null
+++ b/tools/attribution_reporting/BUILD.gn
@@ -0,0 +1,13 @@
+# Copyright 2022 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+executable("attribution_reporting_simulator") {
+  sources = [ "simulator_main.cc" ]
+  deps = [
+    "//base",
+    "//components/version_info",
+    "//content/test:test_support",
+  ]
+  testonly = true
+}
diff --git a/tools/attribution_reporting/DEPS b/tools/attribution_reporting/DEPS
new file mode 100644
index 0000000..40017b0
--- /dev/null
+++ b/tools/attribution_reporting/DEPS
@@ -0,0 +1,4 @@
+include_rules = [
+  "+components/version_info",
+  "+content/public/test",
+]
diff --git a/tools/attribution_reporting/OWNERS b/tools/attribution_reporting/OWNERS
new file mode 100644
index 0000000..6a3b4dd
--- /dev/null
+++ b/tools/attribution_reporting/OWNERS
@@ -0,0 +1 @@
+file://content/browser/attribution_reporting/OWNERS
diff --git a/tools/attribution_reporting/simulator_main.cc b/tools/attribution_reporting/simulator_main.cc
new file mode 100644
index 0000000..491b3aa
--- /dev/null
+++ b/tools/attribution_reporting/simulator_main.cc
@@ -0,0 +1,241 @@
+// Copyright 2022 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include <iostream>
+#include <string>
+
+#include "base/command_line.h"
+#include "base/containers/contains.h"
+#include "base/files/file_path.h"
+#include "base/json/json_file_value_serializer.h"
+#include "base/json/json_writer.h"
+#include "base/values.h"
+#include "components/version_info/version_info.h"
+#include "content/public/test/attribution_simulator.h"
+
+namespace {
+
+constexpr char kSwitchHelp[] = "help";
+constexpr char kSwitchHelpShort[] = "h";
+
+constexpr char kSwitchVersion[] = "version";
+constexpr char kSwitchVersionShort[] = "v";
+
+constexpr char kSwitchInputFile[] = "input_file";
+constexpr char kSwitchRemoveReportIds[] = "remove_report_ids";
+
+constexpr const char* kAllowedSwitches[] = {
+    kSwitchHelp,      kSwitchHelpShort,
+    kSwitchVersion,   kSwitchVersionShort,
+
+    kSwitchInputFile, kSwitchRemoveReportIds,
+};
+
+constexpr const char* kRequiredSwitches[] = {
+    kSwitchInputFile,
+};
+
+constexpr char kHelpMsg[] = R"(
+attribution_reporting_simulator --input_file=<input_file>
+  [--remove_report_ids]
+
+attribution_reporting_simulator is a command-line tool that simulates the
+Attribution Reporting API for a single user on sources and triggers specified
+in an input file. It writes the generated reports, if any, to stdout, with
+associated metadata.
+
+Sources and triggers are registered in chronological order according to their
+`source_time` and `trigger_time` fields, respectively.
+
+Learn more about the Attribution Reporting API at
+https://github.com/WICG/conversion-measurement-api#attribution-reporting-api.
+
+Learn about the meaning of the input and output fields at
+https://github.com/WICG/conversion-measurement-api/blob/main/EVENT.md.
+
+Switches:
+  --input_file=<input_file> - Required path to a JSON file containing sources
+                              and triggers to register in the simulation.
+                              Input format described below.
+  --remove_report_ids       - Optional. If present, removes the `report_id`
+                              field from report bodies, as they are randomly
+                              generated. Use this switch to make the tool's
+                              output more deterministic.
+
+  --version                 - Outputs the tool version and exits.
+
+Input format:
+
+{
+  // List of zero or more sources to register.
+  "sources": [
+    {
+      // Required time at which to register the source in seconds since the
+      // UNIX epoch.
+      "source_time": 123,
+
+      // Required origin on which to register the source.
+      "source_origin": "https://source.example",
+
+      // Required source type, either "navigation" or "event", corresponding to
+      // whether the source is registered on click or on view, respectively.
+      "source_type": "navigation",
+
+      "registration_config": {
+        // Required uint64 formatted as a base-10 string.
+        "source_event_id": "123456789",
+
+        // Required site on which the source will be attributed.
+        "destination": "https://destination.example",
+
+        // Required origin to which the report will be sent if the source is
+        // attributed.
+        "reporting_origin": "https://reporting.example",
+
+        // Optional int64 in milliseconds formatted as a base-10 string.
+        // Defaults to 30 days.
+        "expiry": "864000000",
+
+        // Optional int64 formatted as a base-10 string.
+        // Defaults to 0.
+        "priority": "-456"
+      }
+    },
+    ...
+  ],
+
+  // List of zero or more triggers to register.
+  "triggers": [
+    {
+      // Required time at which to register the trigger in seconds since the
+      // UNIX epoch.
+      "trigger_time": 123,
+
+      // Required site on which the trigger is being registered.
+      "destination": "https://destination.example",
+
+      // Required origin to which the report will be sent.
+      "reporting_origin": "https://reporting.example",
+
+      "registration_config": {
+        // Optional uint64 formatted as a base-10 string.
+        // Defaults to 0.
+        "trigger_data": "3",
+
+        // Optional uint64 formatted as a base-10 string.
+        // Defaults to 0.
+        "event_source_trigger_data": "1",
+
+        // Optional int64 formatted as a base-10 string.
+        // Defaults to 0.
+        "priority": "-456",
+
+        // Optional int64 formatted as a base-10 string.
+        // Defaults to null.
+        "dedup_key": "789"
+      }
+    },
+    ...
+  ]
+}
+
+Output format:
+
+{
+  // List of zero or more reports.
+  reports: [
+    {
+      // Time at which the report would have been sent in seconds since the
+      // UNIX epoch.
+      "report_time": 123,
+
+      // URL to which the report would have been sent.
+      "report_url": "https://reporting.example/.well-known/attribution-reporting/report-attribution",
+
+      // The report itself. See
+      // https://github.com/WICG/conversion-measurement-api/blob/main/EVENT.md#attribution-reports
+      // for details about its fields.
+      "report": { ... }
+      },
+    },
+    ...
+  ]
+}
+)";
+
+void PrintHelp() {
+  std::cerr << kHelpMsg;
+}
+
+}  // namespace
+
+int main(int argc, char* argv[]) {
+  base::CommandLine::Init(argc, argv);
+  const base::CommandLine& command_line =
+      *base::CommandLine::ForCurrentProcess();
+
+  if (!command_line.GetArgs().empty()) {
+    std::cerr << "unexpected additional arguments" << std::endl;
+    PrintHelp();
+    return 1;
+  }
+
+  for (const auto& provided_switch : command_line.GetSwitches()) {
+    if (!base::Contains(kAllowedSwitches, provided_switch.first)) {
+      std::cerr << "unexpected switch `" << provided_switch.first << "`"
+                << std::endl;
+      PrintHelp();
+      return 1;
+    }
+  }
+
+  if (command_line.GetSwitches().empty() ||
+      command_line.HasSwitch(kSwitchHelp) ||
+      command_line.HasSwitch(kSwitchHelpShort)) {
+    PrintHelp();
+    return 0;
+  }
+
+  if (command_line.HasSwitch(kSwitchVersion) ||
+      command_line.HasSwitch(kSwitchVersionShort)) {
+    std::cout << version_info::GetVersionNumber() << std::endl;
+    return 0;
+  }
+
+  for (const char* required_switch : kRequiredSwitches) {
+    if (!command_line.HasSwitch(required_switch)) {
+      std::cerr << "missing required switch `" << required_switch << "`"
+                << std::endl;
+      PrintHelp();
+      return 1;
+    }
+  }
+
+  std::string error_msg;
+  std::unique_ptr<base::Value> input =
+      JSONFileValueDeserializer(
+          command_line.GetSwitchValuePath(kSwitchInputFile))
+          .Deserialize(nullptr, &error_msg);
+  if (!input) {
+    std::cerr << "failed to read input file: " << error_msg << std::endl;
+    return 1;
+  }
+
+  base::Value output = content::RunAttributionSimulationOrExit(
+      *input,
+      content::AttributionSimulationOptions{
+          .remove_report_ids = command_line.HasSwitch(kSwitchRemoveReportIds),
+      });
+
+  std::string output_json;
+  bool success = base::JSONWriter::WriteWithOptions(
+      output, base::JSONWriter::OPTIONS_PRETTY_PRINT, &output_json);
+  if (!success) {
+    std::cerr << "failed to serialize output JSON";
+    return 1;
+  }
+
+  std::cout << output_json;
+  return 0;
+}