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;
+}