blob: 24af3284496f7e6657eb05f954cdc9b0144e0772 [file] [log] [blame]
// Copyright 2020 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/browser/attribution_reporting/attribution_manager_impl.h"
#include <stdint.h>
#include <memory>
#include <utility>
#include <vector>
#include "base/bind.h"
#include "base/callback_helpers.h"
#include "base/check.h"
#include "base/files/scoped_temp_dir.h"
#include "base/guid.h"
#include "base/memory/raw_ptr.h"
#include "base/memory/scoped_refptr.h"
#include "base/run_loop.h"
#include "base/scoped_observation.h"
#include "base/task/sequenced_task_runner.h"
#include "base/test/bind.h"
#include "base/test/metrics/histogram_tester.h"
#include "base/time/clock.h"
#include "base/time/time.h"
#include "build/build_config.h"
#include "content/browser/attribution_reporting/attribution_report.h"
#include "content/browser/attribution_reporting/attribution_storage.h"
#include "content/browser/attribution_reporting/attribution_test_utils.h"
#include "content/browser/attribution_reporting/sent_report_info.h"
#include "content/browser/attribution_reporting/storable_source.h"
#include "content/browser/attribution_reporting/storable_trigger.h"
#include "content/public/test/browser_task_environment.h"
#include "storage/browser/test/mock_special_storage_policy.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "third_party/abseil-cpp/absl/types/optional.h"
namespace content {
namespace {
using CreateReportResult = ::content::AttributionStorage::CreateReportResult;
using CreateReportStatus =
::content::AttributionStorage::CreateReportResult::Status;
using DeactivatedSource = ::content::AttributionStorage::DeactivatedSource;
using ::testing::_;
using ::testing::ElementsAre;
using ::testing::Field;
using ::testing::IsEmpty;
using ::testing::Optional;
using ::testing::Property;
using ::testing::SizeIs;
constexpr base::TimeDelta kExpiredReportOffset = base::Minutes(2);
class ConstantStartupDelayPolicy : public AttributionPolicy {
public:
ConstantStartupDelayPolicy() = default;
~ConstantStartupDelayPolicy() override = default;
base::Time GetReportTimeForReportPastSendTime(base::Time now) const override {
return now + kExpiredReportOffset;
}
};
class TestAttributionManagerObserver : public AttributionManager::Observer {
public:
TestAttributionManagerObserver() = default;
~TestAttributionManagerObserver() override = default;
size_t sources_changed() const { return sources_changed_; }
size_t reports_changed() const { return reports_changed_; }
const std::vector<DeactivatedSource>& deactivated_sources() const {
return deactivated_sources_;
}
const std::vector<SentReportInfo>& sent_reports() const {
return sent_reports_;
}
const std::vector<AttributionStorage::CreateReportResult>& dropped_reports()
const {
return dropped_reports_;
}
private:
// AttributionManager::Observer:
void OnSourcesChanged() override { sources_changed_++; }
void OnReportsChanged() override { reports_changed_++; }
void OnSourceDeactivated(const DeactivatedSource& source) override {
deactivated_sources_.push_back(source);
}
void OnReportSent(const SentReportInfo& info) override {
sent_reports_.push_back(info);
}
void OnReportDropped(
const AttributionStorage::CreateReportResult& result) override {
dropped_reports_.push_back(result);
}
size_t sources_changed_ = 0;
size_t reports_changed_ = 0;
std::vector<DeactivatedSource> deactivated_sources_;
std::vector<SentReportInfo> sent_reports_;
std::vector<AttributionStorage::CreateReportResult> dropped_reports_;
};
// Mock reporter that tracks reports being queued by the AttributionManager.
class TestAttributionReporter
: public AttributionManagerImpl::AttributionReporter {
public:
TestAttributionReporter() = default;
~TestAttributionReporter() override = default;
// AttributionManagerImpl::AttributionReporter
void AddReportsToQueue(std::vector<AttributionReport> reports) override {
if (reports.empty())
return;
for (auto& report : reports) {
added_reports_.push_back(report);
SentReportInfo info(std::move(report), sent_report_info_status_,
/*http_response_code=*/0);
if (should_run_report_sent_callbacks_) {
report_sent_callback_.Run(std::move(info));
} else {
deferred_callbacks_.push_back(std::move(info));
}
}
if (quit_closure_ && added_reports_.size() >= expected_num_reports_)
std::move(quit_closure_).Run();
}
void RunDeferredCallbacks() {
for (auto& deferred_callback : deferred_callbacks_) {
report_sent_callback_.Run(std::move(deferred_callback));
}
deferred_callbacks_.clear();
}
void RemoveAllReportsFromQueue() override {
for (auto& deferred_callback : deferred_callbacks_) {
deferred_callback.status = SentReportInfo::Status::kRemovedFromQueue;
}
RunDeferredCallbacks();
}
void ShouldRunReportSentCallbacks(bool should_run_report_sent_callbacks) {
should_run_report_sent_callbacks_ = should_run_report_sent_callbacks;
}
void SetSentReportInfoStatus(SentReportInfo::Status status) {
sent_report_info_status_ = status;
}
const std::vector<AttributionReport>& added_reports() const {
return added_reports_;
}
void WaitForNumReports(size_t expected_num_reports) {
if (added_reports_.size() >= expected_num_reports)
return;
expected_num_reports_ = expected_num_reports;
base::RunLoop wait_loop;
quit_closure_ = wait_loop.QuitClosure();
wait_loop.Run();
}
void SetReportSentCallback(
base::RepeatingCallback<void(SentReportInfo)> report_sent_callback) {
report_sent_callback_ = std::move(report_sent_callback);
}
private:
base::RepeatingCallback<void(SentReportInfo)> report_sent_callback_;
bool should_run_report_sent_callbacks_ = false;
SentReportInfo::Status sent_report_info_status_ =
SentReportInfo::Status::kSent;
size_t expected_num_reports_ = 0u;
std::vector<AttributionReport> added_reports_;
base::OnceClosure quit_closure_;
std::vector<SentReportInfo> deferred_callbacks_;
};
// Time after impression that a conversion can first be sent. See
// AttributionStorageDelegateImpl::GetReportTimeForConversion().
constexpr base::TimeDelta kFirstReportingWindow = base::Days(2);
// Give impressions a sufficiently long expiry.
constexpr base::TimeDelta kImpressionExpiry = base::Days(30);
} // namespace
class AttributionManagerImplTest : public testing::Test {
public:
AttributionManagerImplTest()
: task_environment_(base::test::TaskEnvironment::TimeSource::MOCK_TIME),
mock_storage_policy_(
base::MakeRefCounted<storage::MockSpecialStoragePolicy>()) {
EXPECT_TRUE(dir_.CreateUniqueTempDir());
CreateManager();
}
void CreateManager() {
auto reporter = std::make_unique<TestAttributionReporter>();
test_reporter_ = reporter.get();
attribution_manager_ = AttributionManagerImpl::CreateForTesting(
std::move(reporter), std::make_unique<ConstantStartupDelayPolicy>(),
task_environment_.GetMockClock(), dir_.GetPath(), mock_storage_policy_);
test_reporter_->SetReportSentCallback(
base::BindRepeating(&AttributionManagerImpl::OnReportSent,
base::Unretained(attribution_manager_.get())));
}
std::vector<StorableSource> StoredSources() {
std::vector<StorableSource> result;
base::RunLoop loop;
attribution_manager_->GetActiveSourcesForWebUI(
base::BindLambdaForTesting([&](std::vector<StorableSource> sources) {
result = std::move(sources);
loop.Quit();
}));
loop.Run();
return result;
}
std::vector<AttributionReport> StoredReports() {
std::vector<AttributionReport> result;
base::RunLoop loop;
attribution_manager_->GetPendingReportsForWebUI(
base::BindLambdaForTesting([&](std::vector<AttributionReport> reports) {
result = std::move(reports);
loop.Quit();
}));
loop.Run();
return result;
}
const base::Clock& clock() { return *task_environment_.GetMockClock(); }
protected:
base::ScopedTempDir dir_;
BrowserTaskEnvironment task_environment_;
std::unique_ptr<AttributionManagerImpl> attribution_manager_;
raw_ptr<TestAttributionReporter> test_reporter_ = nullptr;
scoped_refptr<storage::MockSpecialStoragePolicy> mock_storage_policy_;
};
TEST_F(AttributionManagerImplTest, ImpressionRegistered_ReturnedToWebUI) {
auto impression = SourceBuilder(clock().Now())
.SetExpiry(kImpressionExpiry)
.SetSourceEventId(100)
.Build();
attribution_manager_->HandleSource(impression);
EXPECT_THAT(StoredSources(), ElementsAre(impression));
}
TEST_F(AttributionManagerImplTest, ExpiredImpression_NotReturnedToWebUI) {
attribution_manager_->HandleSource(SourceBuilder(clock().Now())
.SetExpiry(kImpressionExpiry)
.SetSourceEventId(100)
.Build());
task_environment_.FastForwardBy(2 * kImpressionExpiry);
EXPECT_THAT(StoredSources(), IsEmpty());
}
TEST_F(AttributionManagerImplTest, ImpressionConverted_ReportReturnedToWebUI) {
auto impression = SourceBuilder(clock().Now())
.SetExpiry(kImpressionExpiry)
.SetSourceEventId(100)
.Build();
attribution_manager_->HandleSource(impression);
auto conversion = DefaultTrigger();
attribution_manager_->HandleTrigger(conversion);
AttributionReport expected_report =
ReportBuilder(impression)
.SetTriggerData(conversion.trigger_data())
.SetConversionTime(clock().Now())
.SetReportTime(clock().Now() + kFirstReportingWindow)
.Build();
// The external report ID is randomly generated by the storage delegate,
// so zero it out here to avoid flakiness.
std::vector<AttributionReport> reports = StoredReports();
for (auto& report : reports) {
report.external_report_id = DefaultExternalReportID();
}
EXPECT_THAT(reports, ElementsAre(expected_report));
}
TEST_F(AttributionManagerImplTest, ImpressionConverted_ReportQueued) {
attribution_manager_->HandleSource(
SourceBuilder(clock().Now()).SetExpiry(kImpressionExpiry).Build());
attribution_manager_->HandleTrigger(DefaultTrigger());
// Reports are queued in intervals ahead of when they should be
// sent. Make sure the report is not queued earlier than this.
task_environment_.FastForwardBy(kFirstReportingWindow -
kAttributionManagerQueueReportsInterval -
base::Minutes(1));
EXPECT_THAT(test_reporter_->added_reports(), IsEmpty());
task_environment_.FastForwardBy(base::Minutes(1));
EXPECT_THAT(test_reporter_->added_reports(), SizeIs(1));
}
TEST_F(AttributionManagerImplTest, QueuedReportNotSent_NotQueuedAgain) {
attribution_manager_->HandleSource(
SourceBuilder(clock().Now()).SetExpiry(kImpressionExpiry).Build());
attribution_manager_->HandleTrigger(DefaultTrigger());
task_environment_.FastForwardBy(kFirstReportingWindow -
kAttributionManagerQueueReportsInterval);
EXPECT_THAT(test_reporter_->added_reports(), SizeIs(1));
// If the report is not sent, it should not be added to the queue again as
// long as the reporter is still handling it.
task_environment_.FastForwardBy(kAttributionManagerQueueReportsInterval);
EXPECT_THAT(test_reporter_->added_reports(), SizeIs(1));
}
TEST_F(AttributionManagerImplTest,
QueuedReportFailedWithShouldRetry_QueuedAgain) {
base::HistogramTester histograms;
test_reporter_->ShouldRunReportSentCallbacks(true);
test_reporter_->SetSentReportInfoStatus(
SentReportInfo::Status::kTransientFailure);
attribution_manager_->HandleSource(
SourceBuilder(clock().Now()).SetExpiry(kImpressionExpiry).Build());
attribution_manager_->HandleTrigger(DefaultTrigger());
task_environment_.FastForwardBy(kFirstReportingWindow -
kAttributionManagerQueueReportsInterval);
// This is 3 instead of 1 because the failed report is directly added back
// into the queue 2 times.
EXPECT_THAT(test_reporter_->added_reports(), SizeIs(3));
// kFailed = 1.
histograms.ExpectUniqueSample("Conversion.ReportSendOutcome", 1, 1);
}
TEST_F(AttributionManagerImplTest,
QueuedReportFailedWithoutShouldRetry_NotQueuedAgain) {
base::HistogramTester histograms;
test_reporter_->ShouldRunReportSentCallbacks(true);
test_reporter_->SetSentReportInfoStatus(SentReportInfo::Status::kFailure);
attribution_manager_->HandleSource(
SourceBuilder(clock().Now()).SetExpiry(kImpressionExpiry).Build());
attribution_manager_->HandleTrigger(DefaultTrigger());
EXPECT_THAT(StoredReports(), SizeIs(1));
TestAttributionManagerObserver observer;
base::ScopedObservation<AttributionManager, AttributionManager::Observer>
observation(&observer);
observation.Observe(attribution_manager_.get());
task_environment_.FastForwardBy(kFirstReportingWindow -
kAttributionManagerQueueReportsInterval);
EXPECT_THAT(test_reporter_->added_reports(), SizeIs(1));
// Ensure that observers are notified after the report is deleted.
EXPECT_EQ(0u, observer.sources_changed());
EXPECT_EQ(1u, observer.reports_changed());
// If the report indicated retry, it should be added to the queue again.
task_environment_.FastForwardBy(kAttributionManagerQueueReportsInterval);
EXPECT_THAT(test_reporter_->added_reports(), SizeIs(1));
// kFailed = 1.
histograms.ExpectUniqueSample("Conversion.ReportSendOutcome", 1, 1);
}
TEST_F(AttributionManagerImplTest, QueuedReportAlwaysFails_StopsSending) {
base::HistogramTester histograms;
test_reporter_->ShouldRunReportSentCallbacks(false);
test_reporter_->SetSentReportInfoStatus(
SentReportInfo::Status::kTransientFailure);
attribution_manager_->HandleSource(
SourceBuilder(clock().Now()).SetExpiry(kImpressionExpiry).Build());
attribution_manager_->HandleTrigger(DefaultTrigger());
base::Time expected_report_time = clock().Now() + kFirstReportingWindow;
task_environment_.FastForwardBy(kFirstReportingWindow -
kAttributionManagerQueueReportsInterval -
base::Milliseconds(1));
EXPECT_THAT(test_reporter_->added_reports(), IsEmpty());
// The report is first in the queuing window.
task_environment_.FastForwardBy(base::Milliseconds(1));
EXPECT_THAT(test_reporter_->added_reports(),
ElementsAre(Field(&AttributionReport::report_time,
expected_report_time)));
// Simulate the reporter sending the report only once the actual report time
// has been reached.
task_environment_.FastForwardBy(kAttributionManagerQueueReportsInterval);
test_reporter_->RunDeferredCallbacks();
test_reporter_->WaitForNumReports(2);
// At this point, the report has been added directly to the reporter with the
// updated report time of +5 minutes.
expected_report_time += base::Minutes(5);
EXPECT_THAT(test_reporter_->added_reports(),
ElementsAre(_, Field(&AttributionReport::report_time,
expected_report_time)));
task_environment_.FastForwardBy(base::Minutes(5));
EXPECT_THAT(test_reporter_->added_reports(),
ElementsAre(_, Field(&AttributionReport::report_time,
expected_report_time)));
test_reporter_->RunDeferredCallbacks();
test_reporter_->WaitForNumReports(3);
// At this point, the report has been added directly to the reporter with the
// updated report time of +15 minutes.
expected_report_time += base::Minutes(15);
EXPECT_THAT(
test_reporter_->added_reports(),
ElementsAre(
_, _, Field(&AttributionReport::report_time, expected_report_time)));
task_environment_.FastForwardBy(base::Minutes(15));
EXPECT_THAT(
test_reporter_->added_reports(),
ElementsAre(
_, _, Field(&AttributionReport::report_time, expected_report_time)));
test_reporter_->RunDeferredCallbacks();
// At this point, the report has reached the maximum number of attempts and it
// should no longer be present in the DB.
EXPECT_THAT(StoredReports(), IsEmpty());
// kFailed = 1.
histograms.ExpectUniqueSample("Conversion.ReportSendOutcome", 1, 1);
}
TEST_F(AttributionManagerImplTest, QueuedReportOffline_NoFailureIncrement) {
base::HistogramTester histograms;
test_reporter_->ShouldRunReportSentCallbacks(true);
test_reporter_->SetSentReportInfoStatus(
SentReportInfo::Status::kTransientFailure);
attribution_manager_->HandleSource(
SourceBuilder(clock().Now()).SetExpiry(kImpressionExpiry).Build());
attribution_manager_->HandleTrigger(DefaultTrigger());
task_environment_.FastForwardBy(kFirstReportingWindow -
kAttributionManagerQueueReportsInterval);
// This is 3 instead of 1 because the failed report is directly added back
// into the queue 2 times.
EXPECT_THAT(test_reporter_->added_reports(), SizeIs(3));
test_reporter_->SetSentReportInfoStatus(SentReportInfo::Status::kOffline);
task_environment_.FastForwardBy(base::Minutes(30));
EXPECT_THAT(test_reporter_->added_reports(), SizeIs(3));
task_environment_.FastForwardBy(base::Minutes(30));
EXPECT_THAT(test_reporter_->added_reports(), SizeIs(3));
// kFailed =1.
histograms.ExpectUniqueSample("Conversion.ReportSendOutcome", 1, 1);
}
TEST_F(AttributionManagerImplTest, ReportExpiredAtStartup_Sent) {
attribution_manager_->HandleSource(
SourceBuilder(clock().Now()).SetExpiry(kImpressionExpiry).Build());
attribution_manager_->HandleTrigger(DefaultTrigger());
// Simulate shutdown.
attribution_manager_.reset();
// Fast-forward past the reporting window and past report expiry.
task_environment_.FastForwardBy(kFirstReportingWindow);
task_environment_.FastForwardBy(base::Days(100));
// Simulate startup and ensure the report is sent before being expired.
CreateManager();
test_reporter_->WaitForNumReports(1);
EXPECT_THAT(test_reporter_->added_reports(), SizeIs(1));
}
TEST_F(AttributionManagerImplTest, QueuedReportSent_NotQueuedAgain) {
base::HistogramTester histograms;
test_reporter_->ShouldRunReportSentCallbacks(true);
attribution_manager_->HandleSource(
SourceBuilder(clock().Now()).SetExpiry(kImpressionExpiry).Build());
attribution_manager_->HandleTrigger(DefaultTrigger());
task_environment_.FastForwardBy(kFirstReportingWindow -
kAttributionManagerQueueReportsInterval);
EXPECT_THAT(test_reporter_->added_reports(), SizeIs(1));
// The report should not be added to the queue again.
task_environment_.FastForwardBy(kAttributionManagerQueueReportsInterval);
EXPECT_THAT(test_reporter_->added_reports(), SizeIs(1));
// kSent = 0.
histograms.ExpectUniqueSample("Conversion.ReportSendOutcome", 0, 1);
}
TEST_F(AttributionManagerImplTest, QueuedReportSent_ObserversNotified) {
base::HistogramTester histograms;
test_reporter_->ShouldRunReportSentCallbacks(true);
TestAttributionManagerObserver observer;
base::ScopedObservation<AttributionManager, AttributionManager::Observer>
observation(&observer);
observation.Observe(attribution_manager_.get());
test_reporter_->SetSentReportInfoStatus(SentReportInfo::Status::kSent);
attribution_manager_->HandleSource(SourceBuilder(clock().Now())
.SetSourceEventId(1)
.SetExpiry(kImpressionExpiry)
.Build());
attribution_manager_->HandleTrigger(DefaultTrigger());
task_environment_.FastForwardBy(kFirstReportingWindow -
kAttributionManagerQueueReportsInterval);
// This one should be stored, as its status is `kDropped`.
test_reporter_->SetSentReportInfoStatus(SentReportInfo::Status::kDropped);
attribution_manager_->HandleSource(SourceBuilder(clock().Now())
.SetSourceEventId(2)
.SetExpiry(kImpressionExpiry)
.Build());
attribution_manager_->HandleTrigger(DefaultTrigger());
task_environment_.FastForwardBy(kFirstReportingWindow -
kAttributionManagerQueueReportsInterval);
test_reporter_->SetSentReportInfoStatus(SentReportInfo::Status::kSent);
attribution_manager_->HandleSource(SourceBuilder(clock().Now())
.SetSourceEventId(3)
.SetExpiry(kImpressionExpiry)
.Build());
attribution_manager_->HandleTrigger(DefaultTrigger());
task_environment_.FastForwardBy(kFirstReportingWindow -
kAttributionManagerQueueReportsInterval);
// This one shouldn't be stored, as it will be retried.
test_reporter_->SetSentReportInfoStatus(
SentReportInfo::Status::kTransientFailure);
attribution_manager_->HandleSource(SourceBuilder(clock().Now())
.SetSourceEventId(4)
.SetExpiry(kImpressionExpiry)
.Build());
attribution_manager_->HandleTrigger(DefaultTrigger());
task_environment_.FastForwardBy(kFirstReportingWindow -
kAttributionManagerQueueReportsInterval);
EXPECT_THAT(
observer.sent_reports(),
ElementsAre(
Field(&SentReportInfo::report,
Field(&AttributionReport::impression,
Property(&StorableSource::source_event_id, 1u))),
Field(&SentReportInfo::report,
Field(&AttributionReport::impression,
Property(&StorableSource::source_event_id, 2u))),
Field(&SentReportInfo::report,
Field(&AttributionReport::impression,
Property(&StorableSource::source_event_id, 3u)))));
// kSent = 0.
histograms.ExpectBucketCount("Conversion.ReportSendOutcome", 0, 2);
// kFailed = 1.
histograms.ExpectBucketCount("Conversion.ReportSendOutcome", 1, 1);
// kDropped = 2.
histograms.ExpectBucketCount("Conversion.ReportSendOutcome", 2, 1);
}
TEST_F(AttributionManagerImplTest, DroppedReport_ObserversNotified) {
TestAttributionManagerObserver observer;
base::ScopedObservation<AttributionManager, AttributionManager::Observer>
observation(&observer);
observation.Observe(attribution_manager_.get());
attribution_manager_->HandleSource(
SourceBuilder(clock().Now()).SetExpiry(kImpressionExpiry).Build());
EXPECT_THAT(StoredSources(), SizeIs(1));
// `kNavigation` sources can have 3 reports, so none of these should result in
// a dropped report.
for (int i = 1; i <= 3; i++) {
attribution_manager_->HandleTrigger(
TriggerBuilder().SetPriority(i).Build());
EXPECT_THAT(StoredReports(), SizeIs(i));
EXPECT_THAT(observer.dropped_reports(), IsEmpty());
}
{
// This should replace the report with priority 1.
attribution_manager_->HandleTrigger(
TriggerBuilder().SetPriority(4).Build());
EXPECT_THAT(StoredReports(), SizeIs(3));
EXPECT_THAT(
observer.dropped_reports(),
ElementsAre(
AllOf(Property(&CreateReportResult::dropped_report,
Optional(Field(&AttributionReport::priority, 1))),
Property(&CreateReportResult::status,
CreateReportStatus::kSuccessDroppedLowerPriority))));
}
{
// This should be dropped, as it has a lower priority than all stored
// reports.
attribution_manager_->HandleTrigger(
TriggerBuilder().SetPriority(-5).Build());
EXPECT_THAT(StoredReports(), SizeIs(3));
EXPECT_THAT(
observer.dropped_reports(),
ElementsAre(
AllOf(Property(&CreateReportResult::dropped_report,
Optional(Field(&AttributionReport::priority, 1))),
Property(&CreateReportResult::status,
CreateReportStatus::kSuccessDroppedLowerPriority)),
AllOf(Property(&CreateReportResult::dropped_report,
Optional(Field(&AttributionReport::priority, -5))),
Property(&CreateReportResult::status,
CreateReportStatus::kPriorityTooLow))));
}
{
// These should replace the reports with priority 2 and 3.
attribution_manager_->HandleTrigger(
TriggerBuilder().SetPriority(5).Build());
attribution_manager_->HandleTrigger(
TriggerBuilder().SetPriority(6).Build());
EXPECT_THAT(StoredReports(), SizeIs(3));
EXPECT_THAT(
observer.dropped_reports(),
ElementsAre(
AllOf(Property(&CreateReportResult::dropped_report,
Optional(Field(&AttributionReport::priority, 1))),
Property(&CreateReportResult::status,
CreateReportStatus::kSuccessDroppedLowerPriority)),
AllOf(Property(&CreateReportResult::dropped_report,
Optional(Field(&AttributionReport::priority, -5))),
Property(&CreateReportResult::status,
CreateReportStatus::kPriorityTooLow)),
AllOf(Property(&CreateReportResult::dropped_report,
Optional(Field(&AttributionReport::priority, 2))),
Property(&CreateReportResult::status,
CreateReportStatus::kSuccessDroppedLowerPriority)),
AllOf(Property(&CreateReportResult::dropped_report,
Optional(Field(&AttributionReport::priority, 3))),
Property(&CreateReportResult::status,
CreateReportStatus::kSuccessDroppedLowerPriority))));
}
}
// Add a conversion to storage and reset the manager to mimic a report being
// available at startup.
TEST_F(AttributionManagerImplTest, ExpiredReportsAtStartup_Queued) {
// Create a report that will be reported at t= 2 days.
attribution_manager_->HandleSource(
SourceBuilder(clock().Now()).SetExpiry(kImpressionExpiry).Build());
attribution_manager_->HandleTrigger(DefaultTrigger());
// Create another conversion that will be reported at t=
// (kFirstReportingWindow + 2 * kAttributionManagerQueueReportsInterval).
task_environment_.FastForwardBy(2 * kAttributionManagerQueueReportsInterval);
attribution_manager_->HandleSource(
SourceBuilder(clock().Now()).SetExpiry(kImpressionExpiry).Build());
attribution_manager_->HandleTrigger(DefaultTrigger());
EXPECT_THAT(test_reporter_->added_reports(), IsEmpty());
// Reset the manager to simulate shutdown.
attribution_manager_.reset();
// Fast forward past the expected report time of the first conversion, t =
// (kFirstReportingWindow+ 1 minute).
task_environment_.FastForwardBy(
kFirstReportingWindow - (2 * kAttributionManagerQueueReportsInterval) +
base::Minutes(1));
// Create the manager and check that the first report is queued immediately.
CreateManager();
test_reporter_->ShouldRunReportSentCallbacks(true);
test_reporter_->WaitForNumReports(1);
EXPECT_THAT(test_reporter_->added_reports(), SizeIs(1));
// The second report is still queued at the correct time.
task_environment_.FastForwardBy(kAttributionManagerQueueReportsInterval);
EXPECT_THAT(test_reporter_->added_reports(), SizeIs(2));
}
// This functionality is tested more thoroughly in the AttributionStorageSql
// unit tests. Here, just test to make sure the basic control flow is working.
TEST_F(AttributionManagerImplTest, ClearData) {
for (bool match_url : {true, false}) {
base::Time start = clock().Now();
attribution_manager_->HandleSource(
SourceBuilder(start).SetExpiry(kImpressionExpiry).Build());
attribution_manager_->HandleTrigger(DefaultTrigger());
base::RunLoop run_loop;
attribution_manager_->ClearData(
start, start + base::Minutes(1),
base::BindLambdaForTesting(
[match_url](const url::Origin& _) { return match_url; }),
run_loop.QuitClosure());
run_loop.Run();
task_environment_.FastForwardBy(kFirstReportingWindow -
kAttributionManagerQueueReportsInterval);
size_t expected_reports = match_url ? 0u : 1u;
EXPECT_THAT(test_reporter_->added_reports(), SizeIs(expected_reports));
}
}
TEST_F(AttributionManagerImplTest, ConversionsSentFromUI_ReportedImmediately) {
attribution_manager_->HandleSource(
SourceBuilder(clock().Now()).SetExpiry(kImpressionExpiry).Build());
attribution_manager_->HandleTrigger(DefaultTrigger());
EXPECT_THAT(test_reporter_->added_reports(), IsEmpty());
attribution_manager_->SendReportsForWebUI(base::DoNothing());
task_environment_.FastForwardBy(base::Minutes(0));
EXPECT_THAT(test_reporter_->added_reports(), SizeIs(1));
}
// TODO(crbug.com/1088449): Flaky on Linux and Android.
#if defined(OS_LINUX) || defined(OS_CHROMEOS) || defined(OS_ANDROID)
#define MAYBE_ExpiredReportsAtStartup_Delayed \
DISABLED_ExpiredReportsAtStartup_Delayed
#else
#define MAYBE_ExpiredReportsAtStartup_Delayed ExpiredReportsAtStartup_Delayed
#endif
TEST_F(AttributionManagerImplTest, MAYBE_ExpiredReportsAtStartup_Delayed) {
// Create a report that will be reported at t= 2 days.
base::Time start_time = clock().Now();
attribution_manager_->HandleSource(
SourceBuilder(clock().Now()).SetExpiry(kImpressionExpiry).Build());
attribution_manager_->HandleTrigger(DefaultTrigger());
EXPECT_THAT(test_reporter_->added_reports(), IsEmpty());
// Reset the manager to simulate shutdown.
attribution_manager_.reset();
// Fast forward past the expected report time of the first conversion, t =
// (kFirstReportingWindow+ 1 minute).
task_environment_.FastForwardBy(kFirstReportingWindow + base::Minutes(1));
CreateManager();
test_reporter_->WaitForNumReports(1);
// Ensure that the expired report is delayed based on the time the browser
// started.
EXPECT_THAT(test_reporter_->added_reports(),
ElementsAre(Field(&AttributionReport::report_time,
start_time + kFirstReportingWindow +
base::Minutes(1) + kExpiredReportOffset)));
}
TEST_F(AttributionManagerImplTest,
NonExpiredReportsQueuedAtStartup_NotDelayed) {
// Create a report that will be reported at t= 2 days.
base::Time start_time = clock().Now();
attribution_manager_->HandleSource(
SourceBuilder(clock().Now()).SetExpiry(kImpressionExpiry).Build());
attribution_manager_->HandleTrigger(DefaultTrigger());
EXPECT_THAT(test_reporter_->added_reports(), IsEmpty());
// Reset the manager to simulate shutdown.
attribution_manager_.reset();
// Fast forward just before the expected report time.
task_environment_.FastForwardBy(kFirstReportingWindow - base::Minutes(1));
// Ensure that this report does not receive additional delay.
CreateManager();
test_reporter_->WaitForNumReports(1);
EXPECT_THAT(test_reporter_->added_reports(),
ElementsAre(Field(&AttributionReport::report_time,
start_time + kFirstReportingWindow)));
}
TEST_F(AttributionManagerImplTest, SessionOnlyOrigins_DataDeletedAtShutdown) {
GURL session_only_origin("https://sessiononly.example");
auto impression =
SourceBuilder(clock().Now())
.SetImpressionOrigin(url::Origin::Create(session_only_origin))
.Build();
mock_storage_policy_->AddSessionOnly(session_only_origin);
attribution_manager_->HandleSource(impression);
attribution_manager_->HandleTrigger(DefaultTrigger());
EXPECT_THAT(StoredSources(), SizeIs(1));
EXPECT_THAT(StoredReports(), SizeIs(1));
// Reset the manager to simulate shutdown.
attribution_manager_.reset();
CreateManager();
EXPECT_THAT(StoredSources(), IsEmpty());
EXPECT_THAT(StoredReports(), IsEmpty());
}
TEST_F(AttributionManagerImplTest,
SessionOnlyOrigins_DeletedIfAnyOriginMatches) {
url::Origin session_only_origin =
url::Origin::Create(GURL("https://sessiononly.example"));
// Create impressions which each have the session only origin as one of
// impression/conversion/reporting origin.
auto impression1 = SourceBuilder(clock().Now())
.SetImpressionOrigin(session_only_origin)
.Build();
auto impression2 = SourceBuilder(clock().Now())
.SetReportingOrigin(session_only_origin)
.Build();
auto impression3 = SourceBuilder(clock().Now())
.SetConversionOrigin(session_only_origin)
.Build();
// Create one impression which is not session only.
auto impression4 = SourceBuilder(clock().Now()).Build();
mock_storage_policy_->AddSessionOnly(session_only_origin.GetURL());
attribution_manager_->HandleSource(impression1);
attribution_manager_->HandleSource(impression2);
attribution_manager_->HandleSource(impression3);
attribution_manager_->HandleSource(impression4);
EXPECT_THAT(StoredSources(), SizeIs(4));
// Reset the manager to simulate shutdown.
attribution_manager_.reset();
CreateManager();
// All session-only impressions should be deleted.
EXPECT_THAT(StoredSources(), SizeIs(1));
}
// Tests that trigger priority cannot result in more than the maximum number of
// reports being sent. A report will never be queued for the expiry window while
// the source is active given we only queue reports which are reported within
// the next 30 minutes, and the expiry window is one hour after expiry time.
// This ensures that a queued report cannot be overwritten by a new, higher
// priority trigger.
TEST_F(AttributionManagerImplTest, ConversionPrioritization_OneReportSent) {
test_reporter_->ShouldRunReportSentCallbacks(true);
attribution_manager_->HandleSource(
SourceBuilder(clock().Now()).SetExpiry(base::Days(7)).Build());
EXPECT_THAT(StoredSources(), SizeIs(1));
attribution_manager_->HandleTrigger(TriggerBuilder().SetPriority(1).Build());
attribution_manager_->HandleTrigger(TriggerBuilder().SetPriority(1).Build());
attribution_manager_->HandleTrigger(TriggerBuilder().SetPriority(1).Build());
EXPECT_THAT(StoredReports(), SizeIs(3));
task_environment_.FastForwardBy(base::Days(7) - base::Minutes(30));
EXPECT_THAT(test_reporter_->added_reports(), SizeIs(3));
task_environment_.FastForwardBy(base::Minutes(5));
attribution_manager_->HandleTrigger(TriggerBuilder().SetPriority(2).Build());
task_environment_.FastForwardBy(base::Hours(1));
EXPECT_THAT(test_reporter_->added_reports(), SizeIs(3));
}
TEST_F(AttributionManagerImplTest, HandleTrigger_RecordsMetric) {
base::HistogramTester histograms;
attribution_manager_->HandleTrigger(DefaultTrigger());
EXPECT_THAT(StoredReports(), IsEmpty());
histograms.ExpectUniqueSample(
"Conversions.CreateReportStatus",
AttributionStorage::CreateReportResult::Status::kNoMatchingImpressions,
1);
}
TEST_F(AttributionManagerImplTest, OnReportSent_RecordsDeleteEventMetric) {
test_reporter_->ShouldRunReportSentCallbacks(true);
base::HistogramTester histograms;
attribution_manager_->HandleSource(SourceBuilder(clock().Now()).Build());
attribution_manager_->HandleTrigger(DefaultTrigger());
EXPECT_THAT(StoredReports(), SizeIs(1));
TestAttributionManagerObserver observer;
base::ScopedObservation<AttributionManager, AttributionManager::Observer>
observation(&observer);
observation.Observe(attribution_manager_.get());
task_environment_.FastForwardBy(kFirstReportingWindow -
kAttributionManagerQueueReportsInterval);
EXPECT_THAT(StoredReports(), IsEmpty());
// Ensure that deleting a report notifies observers.
EXPECT_EQ(0u, observer.sources_changed());
EXPECT_EQ(1u, observer.reports_changed());
static constexpr char kMetric[] = "Conversions.DeleteSentReportOperation";
histograms.ExpectTotalCount(kMetric, 2);
histograms.ExpectBucketCount(
kMetric, AttributionManagerImpl::DeleteEvent::kStarted, 1);
histograms.ExpectBucketCount(
kMetric, AttributionManagerImpl::DeleteEvent::kSucceeded, 1);
}
TEST_F(AttributionManagerImplTest, ClearData_RequeuesReports) {
const auto origin_a = url::Origin::Create(GURL("https://a.example/"));
const auto origin_b = url::Origin::Create(GURL("https://b.example/"));
attribution_manager_->HandleSource(SourceBuilder(clock().Now())
.SetExpiry(kImpressionExpiry)
.SetReportingOrigin(origin_a)
.Build());
attribution_manager_->HandleTrigger(
TriggerBuilder().SetReportingOrigin(origin_a).Build());
attribution_manager_->HandleSource(SourceBuilder(clock().Now())
.SetExpiry(kImpressionExpiry)
.SetReportingOrigin(origin_b)
.Build());
attribution_manager_->HandleTrigger(
TriggerBuilder().SetReportingOrigin(origin_b).Build());
EXPECT_THAT(test_reporter_->added_reports(), IsEmpty());
task_environment_.FastForwardBy(kFirstReportingWindow -
kAttributionManagerQueueReportsInterval);
test_reporter_->WaitForNumReports(2);
EXPECT_THAT(test_reporter_->added_reports(), SizeIs(2));
attribution_manager_->ClearData(
base::Time::Min(), base::Time::Max(),
base::BindLambdaForTesting(
[&](const url::Origin& origin) { return origin == origin_a; }),
base::DoNothing());
test_reporter_->WaitForNumReports(3);
EXPECT_THAT(test_reporter_->added_reports(), SizeIs(3));
}
TEST_F(AttributionManagerImplTest, ClearData_NoDeleteForRemovedFromQueue) {
const auto origin_a = url::Origin::Create(GURL("https://a.example/"));
const auto origin_b = url::Origin::Create(GURL("https://b.example/"));
attribution_manager_->HandleSource(SourceBuilder(clock().Now())
.SetExpiry(kImpressionExpiry)
.SetReportingOrigin(origin_a)
.Build());
attribution_manager_->HandleTrigger(
TriggerBuilder().SetReportingOrigin(origin_a).Build());
EXPECT_THAT(StoredReports(), SizeIs(1));
task_environment_.FastForwardBy(kFirstReportingWindow -
kAttributionManagerQueueReportsInterval);
test_reporter_->WaitForNumReports(1);
EXPECT_THAT(test_reporter_->added_reports(), SizeIs(1));
EXPECT_THAT(StoredReports(), SizeIs(1));
attribution_manager_->ClearData(
base::Time::Min(), base::Time::Max(),
base::BindLambdaForTesting(
[&](const url::Origin& origin) { return origin == origin_b; }),
base::DoNothing());
EXPECT_THAT(StoredReports(), SizeIs(1));
}
TEST_F(AttributionManagerImplTest, HandleSource_NotifiesObservers) {
TestAttributionManagerObserver observer;
base::ScopedObservation<AttributionManager, AttributionManager::Observer>
observation(&observer);
observation.Observe(attribution_manager_.get());
auto source1 = SourceBuilder(clock().Now())
.SetExpiry(kImpressionExpiry)
.SetSourceEventId(7)
.Build();
attribution_manager_->HandleSource(source1);
EXPECT_THAT(StoredSources(), SizeIs(1));
EXPECT_THAT(observer.deactivated_sources(), IsEmpty());
EXPECT_EQ(1u, observer.sources_changed());
EXPECT_EQ(0u, observer.reports_changed());
attribution_manager_->HandleTrigger(DefaultTrigger());
EXPECT_THAT(StoredReports(), SizeIs(1));
EXPECT_THAT(observer.deactivated_sources(), IsEmpty());
EXPECT_EQ(2u, observer.sources_changed());
EXPECT_EQ(1u, observer.reports_changed());
auto source2 = SourceBuilder(clock().Now())
.SetExpiry(kImpressionExpiry)
.SetSourceEventId(9)
.Build();
attribution_manager_->HandleSource(source2);
EXPECT_THAT(StoredSources(), SizeIs(1));
EXPECT_THAT(observer.deactivated_sources(),
ElementsAre(DeactivatedSource{
source1, DeactivatedSource::Reason::kReplacedByNewerSource}));
EXPECT_EQ(3u, observer.sources_changed());
EXPECT_EQ(1u, observer.reports_changed());
}
TEST_F(AttributionManagerImplTest, HandleTrigger_NotifiesObservers) {
TestAttributionManagerObserver observer;
base::ScopedObservation<AttributionManager, AttributionManager::Observer>
observation(&observer);
observation.Observe(attribution_manager_.get());
test_reporter_->ShouldRunReportSentCallbacks(true);
auto source1 = SourceBuilder(clock().Now())
.SetExpiry(kImpressionExpiry)
.SetSourceEventId(7)
.Build();
attribution_manager_->HandleSource(source1);
EXPECT_THAT(StoredSources(), SizeIs(1));
EXPECT_THAT(observer.deactivated_sources(), IsEmpty());
EXPECT_EQ(1u, observer.sources_changed());
EXPECT_EQ(0u, observer.reports_changed());
// Store the maximum number of reports for the source.
for (size_t i = 1; i <= 3; i++) {
attribution_manager_->HandleTrigger(DefaultTrigger());
EXPECT_THAT(StoredReports(), SizeIs(i));
EXPECT_THAT(observer.deactivated_sources(), IsEmpty());
}
// Each stored report should notify sources changed one time.
EXPECT_EQ(4u, observer.sources_changed());
EXPECT_EQ(3u, observer.reports_changed());
// Simulate the reports being sent and removed from storage.
task_environment_.FastForwardBy(kFirstReportingWindow -
kAttributionManagerQueueReportsInterval);
EXPECT_THAT(StoredReports(), IsEmpty());
// The next report should cause the source to be deactivated; the report
// itself shouldn't be stored as we've already reached the maximum number of
// conversions per source.
attribution_manager_->HandleTrigger(DefaultTrigger());
EXPECT_THAT(StoredReports(), IsEmpty());
EXPECT_THAT(
observer.deactivated_sources(),
ElementsAre(DeactivatedSource{
source1, DeactivatedSource::Reason::kReachedAttributionLimit}));
}
TEST_F(AttributionManagerImplTest, ClearData_NotifiesObservers) {
TestAttributionManagerObserver observer;
base::ScopedObservation<AttributionManager, AttributionManager::Observer>
observation(&observer);
observation.Observe(attribution_manager_.get());
base::RunLoop run_loop;
attribution_manager_->ClearData(
base::Time::Min(), base::Time::Max(),
base::BindRepeating([](const url::Origin& _) { return false; }),
run_loop.QuitClosure());
run_loop.Run();
EXPECT_EQ(1u, observer.sources_changed());
EXPECT_EQ(1u, observer.reports_changed());
}
} // namespace content