[go: nahoru, domu]

blob: 0e66469e52c9819c5c7f579c0ab1c8cd5fb61222 [file] [log] [blame]
// 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 <memory>
#include <ostream>
#include <sstream>
#include <utility>
#include <vector>
#include "base/files/file_path.h"
#include "base/memory/raw_ptr.h"
#include "base/ranges/algorithm.h"
#include "base/run_loop.h"
#include "base/scoped_observation.h"
#include "base/test/bind.h"
#include "base/test/task_environment.h"
#include "base/time/time.h"
#include "base/time/time_to_iso8601.h"
#include "base/values.h"
#include "content/browser/attribution_reporting/attribution_cookie_checker.h"
#include "content/browser/attribution_reporting/attribution_default_random_generator.h"
#include "content/browser/attribution_reporting/attribution_insecure_random_generator.h"
#include "content/browser/attribution_reporting/attribution_manager_impl.h"
#include "content/browser/attribution_reporting/attribution_observer.h"
#include "content/browser/attribution_reporting/attribution_observer_types.h"
#include "content/browser/attribution_reporting/attribution_random_generator.h"
#include "content/browser/attribution_reporting/attribution_report.h"
#include "content/browser/attribution_reporting/attribution_report_sender.h"
#include "content/browser/attribution_reporting/attribution_storage_delegate_impl.h"
#include "content/browser/attribution_reporting/attribution_test_utils.h"
#include "content/browser/attribution_reporting/attribution_trigger.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"
#include "content/browser/storage_partition_impl.h"
#include "content/public/test/browser_task_environment.h"
#include "content/public/test/test_browser_context.h"
#include "content/test/attribution_simulator_input_parser.h"
#include "storage/browser/quota/special_storage_policy.h"
#include "third_party/abseil-cpp/absl/types/variant.h"
#include "url/gurl.h"
namespace content {
namespace {
base::Time GetEventTime(const AttributionSimulationEventAndValue& 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.first);
}
// TODO(apaseltiner): Consider exposing other behaviors here.
class AlwaysSetCookieChecker : public AttributionCookieChecker {
public:
AlwaysSetCookieChecker() = default;
~AlwaysSetCookieChecker() override = default;
AlwaysSetCookieChecker(const AlwaysSetCookieChecker&) = delete;
AlwaysSetCookieChecker(AlwaysSetCookieChecker&&) = delete;
AlwaysSetCookieChecker& operator=(const AlwaysSetCookieChecker&) = delete;
AlwaysSetCookieChecker& operator=(AlwaysSetCookieChecker&&) = delete;
private:
// AttributionManagerImpl::CookieChecker:
void IsDebugCookieSet(const url::Origin& origin,
base::OnceCallback<void(bool)> callback) override {
std::move(callback).Run(true);
}
};
class SentReportAccumulator : public AttributionReportSender {
public:
SentReportAccumulator(base::Value::ListStorage& reports,
base::Value::ListStorage& debug_reports,
bool remove_report_ids,
AttributionReportTimeFormat report_time_format)
: time_origin_(base::Time::Now()),
remove_report_ids_(remove_report_ids),
report_time_format_(report_time_format),
reports_(reports),
debug_reports_(debug_reports) {}
~SentReportAccumulator() override = default;
SentReportAccumulator(const SentReportAccumulator&) = delete;
SentReportAccumulator(SentReportAccumulator&&) = delete;
SentReportAccumulator& operator=(const SentReportAccumulator&) = delete;
SentReportAccumulator& operator=(SentReportAccumulator&&) = delete;
private:
// AttributionManagerImpl::ReportSender:
void SendReport(AttributionReport report,
bool is_debug_report,
ReportSentCallback sent_callback) override {
// TODO(linnan): Support aggregatable reports in the simulator.
if (!absl::holds_alternative<AttributionReport::EventLevelData>(
report.data())) {
return;
}
base::Value report_body = report.ReportBody();
if (remove_report_ids_)
report_body.RemoveKey("report_id");
base::DictionaryValue value;
value.SetKey("report", std::move(report_body));
value.SetStringKey("report_url", report.ReportURL(is_debug_report).spec());
static constexpr char kKeyReportTime[] = "report_time";
base::TimeDelta report_time_delta = base::Time::Now() - time_origin_;
switch (report_time_format_) {
case AttributionReportTimeFormat::kSecondsSinceUnixEpoch:
value.SetIntKey(kKeyReportTime, report_time_delta.InSeconds());
break;
case AttributionReportTimeFormat::kISO8601:
value.SetStringKey(
kKeyReportTime,
base::TimeToISO8601(base::Time::UnixEpoch() + report_time_delta));
break;
}
base::DictionaryValue test_info;
test_info.SetBoolKey("randomized_trigger",
report.attribution_info().source.attribution_logic() ==
StoredSource::AttributionLogic::kFalsely);
value.SetKey("test_info", std::move(test_info));
if (is_debug_report) {
debug_reports_.push_back(std::move(value));
} else {
reports_.push_back(std::move(value));
}
std::move(sent_callback)
.Run(std::move(report), SendResult(SendResult::Status::kSent,
/*http_response_code=*/200));
}
const base::Time time_origin_;
const bool remove_report_ids_;
const AttributionReportTimeFormat report_time_format_;
base::Value::ListStorage& reports_;
base::Value::ListStorage& debug_reports_;
};
// Registers sources and triggers in the `AttributionManagerImpl` and records
// rejected sources in a JSON list.
class AttributionEventHandler : public AttributionObserver {
public:
AttributionEventHandler(AttributionManagerImpl* manager,
base::Value::ListStorage& rejected_sources,
base::Value::ListStorage& rejected_triggers)
: manager_(manager),
rejected_sources_(rejected_sources),
rejected_triggers_(rejected_triggers) {
observation_.Observe(manager);
}
~AttributionEventHandler() override = default;
void Handle(AttributionSimulationEventAndValue event) {
// Sources and triggers are handled in order; this includes observer
// invocations. Therefore, we can track the original `base::Value`
// associated with the event using a queue.
input_values_.push_back(std::move(event.second));
absl::visit(*this, std::move(event.first));
}
// For use with `absl::visit()`.
void operator()(StorableSource source) {
manager_->MaybeEnqueueEventForTesting(std::move(source));
}
// For use with `absl::visit()`.
void operator()(AttributionTriggerAndTime trigger) {
manager_->MaybeEnqueueEventForTesting(std::move(trigger.trigger));
}
private:
// AttributionObserver:
void OnSourceHandled(const StorableSource& source,
StorableSource::Result result) override {
DCHECK(!input_values_.empty());
base::Value input_value = std::move(input_values_.front());
input_values_.pop_front();
std::stringstream reason;
switch (result) {
case StorableSource::Result::kSuccess:
return;
case StorableSource::Result::kInternalError:
case StorableSource::Result::kInsufficientSourceCapacity:
case StorableSource::Result::kInsufficientUniqueDestinationCapacity:
case StorableSource::Result::kExcessiveReportingOrigins:
reason << result;
break;
}
base::DictionaryValue dict;
dict.SetStringKey("reason", reason.str());
dict.SetKey("source", std::move(input_value));
rejected_sources_.push_back(std::move(dict));
}
void OnTriggerHandled(const CreateReportResult& result) override {
DCHECK(!input_values_.empty());
base::Value input_value = std::move(input_values_.front());
input_values_.pop_front();
// TODO(linnan): Support aggregatable reports in the simulator.
std::stringstream reason;
switch (result.event_level_status()) {
case AttributionTrigger::EventLevelResult::kSuccess:
case AttributionTrigger::EventLevelResult::kSuccessDroppedLowerPriority:
// TODO(apaseltiner): Consider surfacing reports dropped due to
// prioritization.
return;
case AttributionTrigger::EventLevelResult::kInternalError:
case AttributionTrigger::EventLevelResult::
kNoCapacityForConversionDestination:
case AttributionTrigger::EventLevelResult::kNoMatchingImpressions:
case AttributionTrigger::EventLevelResult::kDeduplicated:
case AttributionTrigger::EventLevelResult::kExcessiveAttributions:
case AttributionTrigger::EventLevelResult::kPriorityTooLow:
case AttributionTrigger::EventLevelResult::kDroppedForNoise:
case AttributionTrigger::EventLevelResult::kExcessiveReportingOrigins:
case AttributionTrigger::EventLevelResult::kNoMatchingSourceFilterData:
reason << result.event_level_status();
break;
}
base::DictionaryValue dict;
dict.SetStringKey("reason", reason.str());
dict.SetKey("trigger", std::move(input_value));
rejected_triggers_.push_back(std::move(dict));
}
base::ScopedObservation<AttributionManagerImpl, AttributionObserver>
observation_{this};
base::raw_ptr<AttributionManagerImpl> manager_;
base::Value::ListStorage& rejected_sources_;
base::Value::ListStorage& rejected_triggers_;
base::circular_deque<base::Value> input_values_;
};
} // namespace
base::Value RunAttributionSimulation(
base::Value input,
const AttributionSimulationOptions& options,
std::ostream& error_stream) {
// Prerequisites for using an environment with mock time.
content::BrowserTaskEnvironment task_environment(
base::test::TaskEnvironment::TimeSource::MOCK_TIME);
TestBrowserContext browser_context;
absl::optional<AttributionSimulationEventAndValues> events =
ParseAttributionSimulationInput(std::move(input), base::Time::Now(),
error_stream);
if (!events)
return base::Value();
base::ranges::stable_sort(*events, /*comp=*/{}, &GetEventTime);
// Avoid creating an on-disk sqlite DB.
content::AttributionManagerImpl::RunInMemoryForTesting();
// This isn't needed because the DB is completely in memory for testing.
const base::FilePath user_data_directory;
std::unique_ptr<AttributionRandomGenerator> rng;
if (options.noise_seed.has_value()) {
rng = std::make_unique<AttributionInsecureRandomGenerator>(
*options.noise_seed);
} else {
rng = std::make_unique<AttributionDefaultRandomGenerator>();
}
base::Value::ListStorage reports;
base::Value::ListStorage debug_reports;
auto manager = AttributionManagerImpl::CreateForTesting(
user_data_directory,
/*special_storage_policy=*/nullptr,
AttributionStorageDelegateImpl::CreateForTesting(
options.noise_mode, options.delay_mode, std::move(rng),
options.randomized_response_rates),
std::make_unique<AlwaysSetCookieChecker>(),
std::make_unique<SentReportAccumulator>(reports, debug_reports,
options.remove_report_ids,
options.report_time_format),
static_cast<StoragePartitionImpl*>(
browser_context.GetDefaultStoragePartition()));
base::Value::ListStorage rejected_sources;
base::Value::ListStorage rejected_triggers;
AttributionEventHandler handler(manager.get(), rejected_sources,
rejected_triggers);
for (auto& event : *events) {
task_environment.FastForwardBy(GetEventTime(event) - base::Time::Now());
handler.Handle(std::move(event));
}
absl::optional<base::Time> last_report_time;
base::RunLoop loop;
manager->GetPendingReportsForInternalUse(
AttributionReport::ReportType::kEventLevel,
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::Value output(base::Value::Type::DICTIONARY);
output.SetKey("reports", base::Value(std::move(reports)));
if (!debug_reports.empty())
output.SetKey("debug_reports", base::Value(std::move(debug_reports)));
if (!rejected_sources.empty())
output.SetKey("rejected_sources", base::Value(std::move(rejected_sources)));
if (!rejected_triggers.empty()) {
output.SetKey("rejected_triggers",
base::Value(std::move(rejected_triggers)));
}
return output;
}
} // namespace content