| // 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/conversion_manager_impl.h" |
| |
| #include <utility> |
| |
| #include "base/bind.h" |
| #include "base/callback_helpers.h" |
| #include "base/command_line.h" |
| #include "base/memory/ptr_util.h" |
| #include "base/metrics/histogram_functions.h" |
| #include "base/notreached.h" |
| #include "base/task/lazy_thread_pool_task_runner.h" |
| #include "base/threading/sequence_bound.h" |
| #include "base/time/default_clock.h" |
| #include "content/browser/attribution_reporting/attribution_report.h" |
| #include "content/browser/attribution_reporting/conversion_policy.h" |
| #include "content/browser/attribution_reporting/conversion_reporter_impl.h" |
| #include "content/browser/attribution_reporting/conversion_storage_delegate_impl.h" |
| #include "content/browser/attribution_reporting/conversion_storage_sql.h" |
| #include "content/browser/attribution_reporting/storable_source.h" |
| #include "content/browser/attribution_reporting/storable_trigger.h" |
| #include "content/browser/storage_partition_impl.h" |
| #include "content/public/browser/browser_context.h" |
| #include "content/public/browser/storage_partition.h" |
| #include "content/public/browser/web_contents.h" |
| #include "content/public/common/content_switches.h" |
| |
| namespace content { |
| |
| namespace { |
| |
| // These values are persisted to logs. Entries should not be renumbered and |
| // numeric values should never be reused. |
| enum class ConversionReportSendOutcome { |
| kSent = 0, |
| kFailed = 1, |
| kDropped = 2, |
| kMaxValue = kDropped |
| }; |
| |
| const size_t kMaxSentReportsToStore = 100; |
| |
| // The shared-task runner for all conversion storage operations. Note that |
| // different ConversionManagerImpl perform operations on the same task |
| // runner. This prevents any potential races when a given context is destroyed |
| // and recreated for the same backing storage. This uses |
| // BLOCK_SHUTDOWN as some data deletion operations may be running when the |
| // browser is closed, and we want to ensure all data is deleted correctly. |
| base::LazyThreadPoolSequencedTaskRunner g_storage_task_runner = |
| LAZY_THREAD_POOL_SEQUENCED_TASK_RUNNER_INITIALIZER( |
| base::TaskTraits(base::TaskPriority::BEST_EFFORT, |
| base::MayBlock(), |
| base::TaskShutdownBehavior::BLOCK_SHUTDOWN)); |
| |
| bool IsOriginSessionOnly( |
| scoped_refptr<storage::SpecialStoragePolicy> storage_policy, |
| const url::Origin& origin) { |
| // TODO(johnidel): This conversion is unfortunate but necessary. Storage |
| // partition clear data logic uses Origin keyed deletion, while the storage |
| // policy uses GURLs. Ideally these would be coalesced. |
| const GURL& url = origin.GetURL(); |
| if (storage_policy->IsStorageProtected(url)) |
| return false; |
| |
| if (storage_policy->IsStorageSessionOnly(url)) |
| return true; |
| return false; |
| } |
| |
| void RecordCreateReportStatus( |
| ConversionStorage::CreateReportResult::Status status) { |
| base::UmaHistogramEnumeration("Conversions.CreateReportStatus", status); |
| } |
| |
| // We measure this in order to be able to count reports that weren't |
| // successfully deleted, which can lead to duplicate reports. |
| void RecordDeleteEvent(ConversionManagerImpl::DeleteEvent event) { |
| base::UmaHistogramEnumeration("Conversions.DeleteSentReportOperation", event); |
| } |
| |
| ConversionReportSendOutcome ConvertToConversionReportSendOutcome( |
| SentReportInfo::Status status) { |
| switch (status) { |
| case SentReportInfo::Status::kSent: |
| return ConversionReportSendOutcome::kSent; |
| case SentReportInfo::Status::kTransientFailure: |
| case SentReportInfo::Status::kFailure: |
| return ConversionReportSendOutcome::kFailed; |
| case SentReportInfo::Status::kOffline: |
| case SentReportInfo::Status::kRemovedFromQueue: |
| // Offline reports and reports removed from the queue before being sent |
| // should never record an outcome. |
| NOTREACHED(); |
| return ConversionReportSendOutcome::kFailed; |
| case SentReportInfo::Status::kDropped: |
| return ConversionReportSendOutcome::kDropped; |
| } |
| } |
| |
| } // namespace |
| |
| const constexpr base::TimeDelta kConversionManagerQueueReportsInterval = |
| base::Minutes(30); |
| |
| ConversionManager* ConversionManagerProviderImpl::GetManager( |
| WebContents* web_contents) const { |
| return static_cast<StoragePartitionImpl*>( |
| web_contents->GetBrowserContext()->GetDefaultStoragePartition()) |
| ->GetConversionManager(); |
| } |
| |
| // static |
| void ConversionManagerImpl::RunInMemoryForTesting() { |
| ConversionStorageSql::RunInMemoryForTesting(); |
| } |
| |
| // static |
| std::unique_ptr<ConversionManagerImpl> ConversionManagerImpl::CreateForTesting( |
| std::unique_ptr<ConversionReporter> reporter, |
| std::unique_ptr<ConversionPolicy> policy, |
| const base::Clock* clock, |
| const base::FilePath& user_data_directory, |
| scoped_refptr<storage::SpecialStoragePolicy> special_storage_policy, |
| size_t max_sent_reports_to_store) { |
| return base::WrapUnique<ConversionManagerImpl>(new ConversionManagerImpl( |
| std::move(reporter), std::move(policy), clock, user_data_directory, |
| std::move(special_storage_policy), max_sent_reports_to_store)); |
| } |
| |
| ConversionManagerImpl::ConversionManagerImpl( |
| StoragePartitionImpl* storage_partition, |
| const base::FilePath& user_data_directory, |
| scoped_refptr<storage::SpecialStoragePolicy> special_storage_policy) |
| : ConversionManagerImpl( |
| std::make_unique<ConversionReporterImpl>( |
| storage_partition, |
| base::DefaultClock::GetInstance(), |
| // |reporter_| is owned by |this|, so `base::Unretained()` is safe |
| // as the reporter and callbacks will be deleted first. |
| base::BindRepeating(&ConversionManagerImpl::OnReportSent, |
| base::Unretained(this))), |
| std::make_unique<ConversionPolicy>( |
| base::CommandLine::ForCurrentProcess()->HasSwitch( |
| switches::kConversionsDebugMode)), |
| base::DefaultClock::GetInstance(), |
| user_data_directory, |
| std::move(special_storage_policy), |
| kMaxSentReportsToStore) {} |
| |
| ConversionManagerImpl::ConversionManagerImpl( |
| std::unique_ptr<ConversionReporter> reporter, |
| std::unique_ptr<ConversionPolicy> policy, |
| const base::Clock* clock, |
| const base::FilePath& user_data_directory, |
| scoped_refptr<storage::SpecialStoragePolicy> special_storage_policy, |
| size_t max_sent_reports_to_store) |
| : debug_mode_(base::CommandLine::ForCurrentProcess()->HasSwitch( |
| switches::kConversionsDebugMode)), |
| clock_(clock), |
| reporter_(std::move(reporter)), |
| conversion_storage_(base::SequenceBound<ConversionStorageSql>( |
| g_storage_task_runner.Get(), |
| user_data_directory, |
| std::make_unique<ConversionStorageDelegateImpl>(debug_mode_), |
| clock_)), |
| session_storage_(max_sent_reports_to_store), |
| conversion_policy_(std::move(policy)), |
| special_storage_policy_(std::move(special_storage_policy)), |
| weak_factory_(this) { |
| // Once the database is loaded, get all reports that may have expired while |
| // Chrome was not running and handle these specially. It is safe to post tasks |
| // to the storage context as soon as it is created. |
| GetAndHandleReports(base::BindOnce(&ConversionManagerImpl::QueueReports, |
| weak_factory_.GetWeakPtr()), |
| clock_->Now() + kConversionManagerQueueReportsInterval); |
| |
| // Start a repeating timer that will fetch reports once every |
| // |kConversionManagerQueueReportsInterval| and add them to |reporter_|. |
| get_and_queue_reports_timer_.Start( |
| FROM_HERE, kConversionManagerQueueReportsInterval, this, |
| &ConversionManagerImpl::GetAndQueueReportsForNextInterval); |
| } |
| |
| ConversionManagerImpl::~ConversionManagerImpl() { |
| // Browser contexts are not required to have a special storage policy. |
| if (!special_storage_policy_ || |
| !special_storage_policy_->HasSessionOnlyOrigins()) { |
| return; |
| } |
| |
| // Delete stored data for all session only origins given by |
| // |special_storage_policy|. |
| base::RepeatingCallback<bool(const url::Origin&)> |
| session_only_origin_predicate = base::BindRepeating( |
| &IsOriginSessionOnly, std::move(special_storage_policy_)); |
| conversion_storage_.AsyncCall(&ConversionStorage::ClearData) |
| .WithArgs(base::Time::Min(), base::Time::Max(), |
| std::move(session_only_origin_predicate)); |
| } |
| |
| void ConversionManagerImpl::HandleImpression(StorableSource impression) { |
| // Add the impression to storage. |
| conversion_storage_.AsyncCall(&ConversionStorage::StoreImpression) |
| .WithArgs(std::move(impression)); |
| } |
| |
| void ConversionManagerImpl::HandleConversion(StorableTrigger conversion) { |
| conversion_storage_ |
| .AsyncCall(&ConversionStorage::MaybeCreateAndStoreConversionReport) |
| .WithArgs(std::move(conversion)) |
| .Then(base::BindOnce(&ConversionManagerImpl::OnReportStored, |
| weak_factory_.GetWeakPtr())); |
| |
| // If we are running in debug mode, we should also schedule a task to |
| // gather and send any new reports. |
| if (debug_mode_) |
| GetAndQueueReportsForNextInterval(); |
| } |
| |
| void ConversionManagerImpl::OnReportStored( |
| ConversionStorage::CreateReportResult result) { |
| RecordCreateReportStatus(result.status); |
| if (!result.dropped_report.has_value()) |
| return; |
| |
| session_storage_.AddDroppedReport(std::move(*result.dropped_report)); |
| } |
| |
| void ConversionManagerImpl::GetActiveImpressionsForWebUI( |
| base::OnceCallback<void(std::vector<StorableSource>)> callback) { |
| const int kMaxImpressions = 1000; |
| conversion_storage_.AsyncCall(&ConversionStorage::GetActiveImpressions) |
| .WithArgs(kMaxImpressions) |
| .Then(std::move(callback)); |
| } |
| |
| void ConversionManagerImpl::GetPendingReportsForWebUI( |
| base::OnceCallback<void(std::vector<AttributionReport>)> callback, |
| base::Time max_report_time) { |
| const int kMaxReports = 1000; |
| GetAndHandleReports(std::move(callback), max_report_time, kMaxReports); |
| } |
| |
| const ConversionSessionStorage& ConversionManagerImpl::GetSessionStorage() |
| const { |
| return session_storage_; |
| } |
| |
| void ConversionManagerImpl::SendReportsForWebUI(base::OnceClosure done) { |
| GetAndHandleReports( |
| base::BindOnce(&ConversionManagerImpl::HandleReportsSentFromWebUI, |
| weak_factory_.GetWeakPtr(), std::move(done)), |
| base::Time::Max()); |
| } |
| |
| const ConversionPolicy& ConversionManagerImpl::GetConversionPolicy() const { |
| return *conversion_policy_; |
| } |
| |
| void ConversionManagerImpl::ClearData( |
| base::Time delete_begin, |
| base::Time delete_end, |
| base::RepeatingCallback<bool(const url::Origin&)> filter, |
| base::OnceClosure done) { |
| session_storage_.Reset(); |
| reporter_->RemoveAllReportsFromQueue(); |
| conversion_storage_.AsyncCall(&ConversionStorage::ClearData) |
| .WithArgs(delete_begin, delete_end, std::move(filter)) |
| .Then(base::BindOnce( |
| [](base::OnceClosure done, |
| base::WeakPtr<ConversionManagerImpl> manager) { |
| std::move(done).Run(); |
| if (manager) |
| manager->GetAndQueueReportsForNextInterval(); |
| }, |
| std::move(done), weak_factory_.GetWeakPtr())); |
| } |
| |
| void ConversionManagerImpl::GetAndHandleReports( |
| ReportsHandlerFunc handler_function, |
| base::Time max_report_time, |
| int limit) { |
| conversion_storage_.AsyncCall(&ConversionStorage::GetConversionsToReport) |
| .WithArgs(max_report_time, limit) |
| .Then(std::move(handler_function)); |
| } |
| |
| void ConversionManagerImpl::GetAndQueueReportsForNextInterval() { |
| // Get all the reports that will be reported in the next interval and them to |
| // the |reporter_|. |
| GetAndHandleReports(base::BindOnce(&ConversionManagerImpl::QueueReports, |
| weak_factory_.GetWeakPtr()), |
| clock_->Now() + kConversionManagerQueueReportsInterval); |
| } |
| |
| void ConversionManagerImpl::QueueReports( |
| std::vector<AttributionReport> reports) { |
| if (reports.empty()) |
| return; |
| |
| // Add delay to all reports that expired while the browser was not able to |
| // send reports (or not running) so they are not temporally joinable. |
| base::Time current_time = clock_->Now(); |
| for (AttributionReport& report : reports) { |
| if (report.report_time >= current_time) |
| continue; |
| |
| report.report_time = |
| conversion_policy_->GetReportTimeForReportPastSendTime(current_time); |
| } |
| |
| reporter_->AddReportsToQueue(std::move(reports)); |
| } |
| |
| void ConversionManagerImpl::HandleReportsSentFromWebUI( |
| base::OnceClosure done, |
| std::vector<AttributionReport> reports) { |
| // If there's already a send-all in progress, ignore this request. |
| if (reports.empty() || !send_reports_for_web_ui_callback_.is_null()) { |
| std::move(done).Run(); |
| return; |
| } |
| |
| std::vector<AttributionReport::Id> conversion_ids; |
| conversion_ids.reserve(reports.size()); |
| base::Time now = clock_->Now(); |
| // All reports should be sent immediately. |
| for (AttributionReport& report : reports) { |
| report.report_time = now; |
| DCHECK(report.conversion_id.has_value()); |
| conversion_ids.push_back(*report.conversion_id); |
| } |
| |
| // Reports may already be in the process of sending, but they will be |
| // deduplicated by `ConversionReporterImpl::AddReportsToQueue()`. In that |
| // case, the callback will be invoked as a result of the in-process reports. |
| pending_conversion_ids_for_internals_ui_ = |
| base::flat_set<AttributionReport::Id>(std::move(conversion_ids)); |
| send_reports_for_web_ui_callback_ = std::move(done); |
| |
| reporter_->AddReportsToQueue(std::move(reports)); |
| } |
| |
| void ConversionManagerImpl::OnReportSent(SentReportInfo info) { |
| DCHECK(info.report.conversion_id.has_value()); |
| |
| // If there was a transient failure, and another attempt is allowed, |
| // update the report's DB state to reflect that. Otherwise, delete the report |
| // from storage if it wasn't skipped due to the browser being offline. |
| |
| bool should_retry = false; |
| if (info.status == SentReportInfo::Status::kTransientFailure) { |
| info.report.failed_send_attempts++; |
| const absl::optional<base::TimeDelta> delay = |
| conversion_policy_->GetFailedReportDelay( |
| info.report.failed_send_attempts); |
| if (delay.has_value()) { |
| should_retry = true; |
| info.report.report_time += *delay; |
| } |
| } |
| |
| if (should_retry) { |
| // After updating the report's failure count and new report time in the DB, |
| // add it directly to the queue so that the retry is attempted as the new |
| // report time is reached, rather than wait for the next DB-polling to |
| // occur. |
| conversion_storage_ |
| .AsyncCall(&ConversionStorage::UpdateReportForSendFailure) |
| .WithArgs(*info.report.conversion_id, info.report.report_time) |
| .Then(base::BindOnce( |
| [](base::WeakPtr<ConversionManagerImpl> manager, |
| AttributionReport report, bool success) { |
| if (!manager || !success) |
| return; |
| manager->reporter_->AddReportsToQueue({std::move(report)}); |
| }, |
| weak_factory_.GetWeakPtr(), info.report)); |
| } else if (info.status != SentReportInfo::Status::kOffline && |
| info.status != SentReportInfo::Status::kRemovedFromQueue) { |
| RecordDeleteEvent(DeleteEvent::kStarted); |
| conversion_storage_.AsyncCall(&ConversionStorage::DeleteConversion) |
| .WithArgs(*info.report.conversion_id) |
| .Then(base::BindOnce([](bool succeeded) { |
| RecordDeleteEvent(succeeded ? DeleteEvent::kSucceeded |
| : DeleteEvent::kFailed); |
| })); |
| |
| base::UmaHistogramEnumeration( |
| "Conversion.ReportSendOutcome", |
| ConvertToConversionReportSendOutcome(info.status)); |
| } |
| |
| DCHECK_EQ(send_reports_for_web_ui_callback_.is_null(), |
| pending_conversion_ids_for_internals_ui_.empty()); |
| |
| // If there's a `SendReportsForWebUI()` callback waiting on this report's |
| // conversion ID, remove the ID from the wait-set; if it was the last such ID, |
| // run the callback. |
| if (!send_reports_for_web_ui_callback_.is_null() && |
| pending_conversion_ids_for_internals_ui_.erase( |
| *info.report.conversion_id) > 0 && |
| pending_conversion_ids_for_internals_ui_.empty()) { |
| std::move(send_reports_for_web_ui_callback_).Run(); |
| } |
| |
| // TODO(apaseltiner): Consider surfacing retry attempts in internals UI. |
| if (info.status != SentReportInfo::Status::kSent && |
| info.status != SentReportInfo::Status::kFailure) |
| return; |
| |
| session_storage_.AddSentReport(std::move(info)); |
| } |
| |
| } // namespace content |