blob: 7a6996282fa2802ee19a8f77f814229591715248 [file] [log] [blame]
// Copyright 2025 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "content/browser/private_aggregation/private_aggregation_pending_contributions.h"
#include <stddef.h>
#include <stdint.h>
#include <algorithm>
#include <iterator>
#include <map>
#include <memory>
#include <set>
#include <string_view>
#include <utility>
#include <variant>
#include <vector>
#include "base/check.h"
#include "base/check_op.h"
#include "base/containers/contains.h"
#include "base/feature_list.h"
#include "base/metrics/histogram_functions.h"
#include "base/numerics/safe_conversions.h"
#include "base/strings/strcat.h"
#include "third_party/blink/public/common/features_generated.h"
#include "third_party/blink/public/mojom/aggregation_service/aggregatable_report.mojom.h"
#include "third_party/blink/public/mojom/private_aggregation/private_aggregation_host.mojom.h"
namespace content {
using PAErrorEvent = blink::mojom::PrivateAggregationErrorEvent;
PrivateAggregationPendingContributions::PrivateAggregationPendingContributions(
base::StrictNumeric<size_t> max_contributions,
std::vector<std::string_view> histogram_suffixes)
: max_contributions_(max_contributions),
histogram_suffixes_(histogram_suffixes) {
CHECK(base::FeatureList::IsEnabled(
blink::features::kPrivateAggregationApiErrorReporting));
}
PrivateAggregationPendingContributions::PrivateAggregationPendingContributions(
PrivateAggregationPendingContributions&& other) = default;
PrivateAggregationPendingContributions&
PrivateAggregationPendingContributions::operator=(
PrivateAggregationPendingContributions&& other) = default;
PrivateAggregationPendingContributions::
~PrivateAggregationPendingContributions() = default;
bool PrivateAggregationPendingContributions::IsEmpty() const {
return unconditional_contributions_.empty() &&
conditional_contributions_.empty();
}
const std::map<
blink::mojom::PrivateAggregationErrorEvent,
std::vector<blink::mojom::AggregatableReportHistogramContribution>>&
PrivateAggregationPendingContributions::GetConditionalContributionsForTesting()
const {
return conditional_contributions_;
}
void PrivateAggregationPendingContributions::AddUnconditionalContributions(
std::vector<blink::mojom::AggregatableReportHistogramContribution>
contributions) {
CHECK(!are_contributions_finalized_);
std::erase_if(contributions, [](auto& elem) { return elem.value == 0; });
unconditional_contributions_.reserve(unconditional_contributions_.size() +
contributions.size());
std::ranges::transform(
contributions, std::back_inserter(unconditional_contributions_),
[](auto& contribution) { return std::move(contribution); });
}
void PrivateAggregationPendingContributions::AddConditionalContributions(
PAErrorEvent error_event,
std::vector<blink::mojom::AggregatableReportHistogramContribution>
contributions) {
CHECK(!are_contributions_finalized_);
std::erase_if(contributions, [](auto& elem) { return elem.value == 0; });
std::vector<blink::mojom::AggregatableReportHistogramContribution>&
destination = conditional_contributions_[error_event];
destination.reserve(destination.size() + contributions.size());
std::ranges::transform(
contributions, std::back_inserter(destination),
[](auto& contribution) { return std::move(contribution); });
}
void PrivateAggregationPendingContributions::AddToFinalUnmergedContributions(
std::vector<blink::mojom::AggregatableReportHistogramContribution>
contributions,
std::set<ContributionMergeKey>& accepted_merge_keys,
std::set<ContributionMergeKey>& truncated_merge_keys) {
for (blink::mojom::AggregatableReportHistogramContribution& contribution :
contributions) {
ContributionMergeKey merge_key(contribution);
if (base::Contains(accepted_merge_keys, merge_key)) {
// Bound worst-case memory usage.
constexpr size_t kMaxUnmergedContributions = 10'000;
if (final_unmerged_contributions_.size() < kMaxUnmergedContributions) {
final_unmerged_contributions_.push_back(std::move(contribution));
}
} else if (accepted_merge_keys.size() == max_contributions_) {
// Drop the contribution, there's no space left.
// Bound worst-case memory usage.
constexpr size_t kMaxTruncatedMergeKeysTracked = 10'000;
if (truncated_merge_keys.size() < kMaxTruncatedMergeKeysTracked) {
truncated_merge_keys.insert(std::move(merge_key));
}
continue;
} else {
accepted_merge_keys.insert(std::move(merge_key));
final_unmerged_contributions_.push_back(std::move(contribution));
}
}
}
void PrivateAggregationPendingContributions::MarkContributionsFinalized(
TimeoutOrDisconnect finalization_cause) {
CHECK(!are_contributions_finalized_);
are_contributions_finalized_ = true;
was_error_triggered_[PAErrorEvent::kContributionTimeoutReached] =
(finalization_cause == TimeoutOrDisconnect::kTimeout);
}
void PrivateAggregationPendingContributions::ApplyTestBudgeterResults(
std::vector<BudgeterResult> results,
PendingReportLimitResult pending_report_limit_result,
NullReportBehavior null_report_behavior) {
CHECK(are_contributions_finalized_);
CHECK_EQ(results.size(), unconditional_contributions_.size());
bool insufficient_budget = false;
std::set<ContributionMergeKey> approved_merge_keys;
for (size_t i = 0; i < results.size(); ++i) {
if (results[i] == BudgeterResult::kApproved) {
approved_merge_keys.insert(
ContributionMergeKey(unconditional_contributions_[i]));
} else {
// Signal to delete this later.
unconditional_contributions_[i].value = 0;
insufficient_budget = true;
}
}
std::erase_if(unconditional_contributions_,
[](auto& contribution) { return contribution.value == 0; });
bool pending_report_limit_reached =
(pending_report_limit_result == PendingReportLimitResult::kAtLimit);
bool empty_report_dropped =
(unconditional_contributions_.empty() &&
null_report_behavior == NullReportBehavior::kDontSendReport);
bool too_many_contributions = approved_merge_keys.size() > max_contributions_;
bool report_success = !empty_report_dropped && !too_many_contributions &&
!insufficient_budget && !pending_report_limit_reached;
// Ensure this function is only called once.
CHECK_EQ(was_error_triggered_.size(), 1u);
was_error_triggered_[PAErrorEvent::kInsufficientBudget] = insufficient_budget;
was_error_triggered_[PAErrorEvent::kPendingReportLimitReached] =
pending_report_limit_reached;
was_error_triggered_[PAErrorEvent::kEmptyReportDropped] =
empty_report_dropped;
was_error_triggered_[PAErrorEvent::kTooManyContributions] =
too_many_contributions;
was_error_triggered_[PAErrorEvent::kReportSuccess] = report_success;
}
const std::vector<blink::mojom::AggregatableReportHistogramContribution>&
PrivateAggregationPendingContributions::CompileFinalUnmergedContributions(
std::vector<BudgeterResult> test_budgeter_results,
PendingReportLimitResult pending_report_limit_result,
NullReportBehavior null_report_behavior) {
ApplyTestBudgeterResults(std::move(test_budgeter_results),
pending_report_limit_result, null_report_behavior);
was_error_triggered_[PAErrorEvent::kAlreadyTriggeredExternalError] = true;
std::set<ContributionMergeKey> accepted_merge_keys;
std::set<ContributionMergeKey> truncated_merge_keys;
CHECK(final_unmerged_contributions_.empty());
for (auto i = static_cast<int>(PAErrorEvent::kMinValue);
i <= static_cast<int>(PAErrorEvent::kMaxValue); i++) {
auto error_event = static_cast<PAErrorEvent>(i);
CHECK(base::Contains(was_error_triggered_, error_event));
if (was_error_triggered_[error_event]) {
AddToFinalUnmergedContributions(
std::move(conditional_contributions_[error_event]),
accepted_merge_keys, truncated_merge_keys);
}
// No need to keep in memory any longer
conditional_contributions_.erase(error_event);
}
size_t num_truncations_due_to_unconditional_contributions = 0;
{
std::set<ContributionMergeKey> unconditional_contribution_merge_keys;
for (const blink::mojom::AggregatableReportHistogramContribution&
contribution : unconditional_contributions_) {
unconditional_contribution_merge_keys.insert(
ContributionMergeKey(contribution));
}
if (unconditional_contribution_merge_keys.size() > max_contributions_) {
num_truncations_due_to_unconditional_contributions =
unconditional_contribution_merge_keys.size() - max_contributions_;
}
}
// Unconditional contributions come last to prioritize successful measurement
// of errors.
AddToFinalUnmergedContributions(std::move(unconditional_contributions_),
accepted_merge_keys, truncated_merge_keys);
RecordNumberOfContributionMergeKeysHistogram(
/*num_merge_keys_sent_or_truncated=*/accepted_merge_keys.size() +
truncated_merge_keys.size());
RecordNumberOfFinalUnmergedContributionsHistogram(
/*num_final_unmerged_contributions=*/final_unmerged_contributions_
.size());
RecordTruncationHistogram(num_truncations_due_to_unconditional_contributions,
/*total_truncations=*/truncated_merge_keys.size());
return final_unmerged_contributions_;
}
void PrivateAggregationPendingContributions::ApplyFinalBudgeterResults(
std::vector<BudgeterResult> results) {
CHECK_EQ(results.size(), final_unmerged_contributions_.size());
for (size_t i = 0; i < results.size(); ++i) {
if (results[i] != BudgeterResult::kApproved) {
// Signals this contribution should be deleted below.
final_unmerged_contributions_[i].value = 0;
}
}
std::erase_if(final_unmerged_contributions_,
[](auto& contribution) { return contribution.value == 0; });
}
std::vector<blink::mojom::AggregatableReportHistogramContribution>
PrivateAggregationPendingContributions::TakeFinalContributions(
std::vector<BudgeterResult> final_budgeter_results) && {
ApplyFinalBudgeterResults(std::move(final_budgeter_results));
std::map<ContributionMergeKey, size_t> indices_of_accepted_keys;
std::vector<blink::mojom::AggregatableReportHistogramContribution>
final_contributions;
for (blink::mojom::AggregatableReportHistogramContribution& contribution :
final_unmerged_contributions_) {
ContributionMergeKey merge_key(contribution);
auto indices_of_accepted_keys_it = indices_of_accepted_keys.find(merge_key);
if (indices_of_accepted_keys_it != indices_of_accepted_keys.end()) {
// As this contribution has the same bucket and filtering_id as an
// earlier contribution, we can just sum the values.
blink::mojom::AggregatableReportHistogramContribution& vector_entry =
final_contributions[indices_of_accepted_keys[merge_key]];
CHECK(ContributionMergeKey(vector_entry) == merge_key);
vector_entry.value =
base::ClampedNumeric(vector_entry.value) + contribution.value;
} else {
// No need to bound worst-case memory usage, we've already limited the
// maximum size of this list to `max_contributions_`.
indices_of_accepted_keys.emplace(std::move(merge_key),
final_contributions.size());
final_contributions.push_back(std::move(contribution));
}
}
return final_contributions;
}
void PrivateAggregationPendingContributions::
RecordNumberOfContributionMergeKeysHistogram(
size_t num_merge_keys_sent_or_truncated) const {
constexpr std::string_view kMergeKeysHistogramBase =
"PrivacySandbox.PrivateAggregation.NumContributionMergeKeys";
base::UmaHistogramCounts10000(kMergeKeysHistogramBase,
num_merge_keys_sent_or_truncated);
for (std::string_view histogram_suffix : histogram_suffixes_) {
base::UmaHistogramCounts10000(
base::StrCat({kMergeKeysHistogramBase, histogram_suffix}),
num_merge_keys_sent_or_truncated);
}
}
void PrivateAggregationPendingContributions::
RecordNumberOfFinalUnmergedContributionsHistogram(
size_t num_final_unmerged_contributions) const {
constexpr std::string_view kFinalUnmergedContributionsBase =
"PrivacySandbox.PrivateAggregation.NumFinalUnmergedContributions";
base::UmaHistogramCounts10000(kFinalUnmergedContributionsBase,
num_final_unmerged_contributions);
for (std::string_view histogram_suffix : histogram_suffixes_) {
base::UmaHistogramCounts10000(
base::StrCat({kFinalUnmergedContributionsBase, histogram_suffix}),
num_final_unmerged_contributions);
}
}
void PrivateAggregationPendingContributions::RecordTruncationHistogram(
size_t num_truncations_due_to_unconditional_contributions,
size_t total_truncations) const {
TruncationResult truncation_result;
if (total_truncations == 0) {
truncation_result = TruncationResult::kNoTruncation;
} else if (num_truncations_due_to_unconditional_contributions > 0) {
truncation_result =
TruncationResult::kTruncationDueToUnconditionalContributions;
} else {
truncation_result =
TruncationResult::kTruncationNotDueToUnconditionalContributions;
}
base::UmaHistogramEnumeration(
"PrivacySandbox.PrivateAggregation.TruncationResult", truncation_result);
}
PrivateAggregationPendingContributions::Wrapper::Wrapper(
PrivateAggregationPendingContributions pending_contributions)
: contributions_(std::move(pending_contributions)) {
CHECK(base::FeatureList::IsEnabled(
blink::features::kPrivateAggregationApiErrorReporting));
}
PrivateAggregationPendingContributions::Wrapper::Wrapper(
std::vector<blink::mojom::AggregatableReportHistogramContribution>
contributions_vector)
: contributions_(std::move(contributions_vector)) {
CHECK(!base::FeatureList::IsEnabled(
blink::features::kPrivateAggregationApiErrorReporting));
}
PrivateAggregationPendingContributions::Wrapper::Wrapper(
PrivateAggregationPendingContributions::Wrapper&& other) = default;
PrivateAggregationPendingContributions::Wrapper&
PrivateAggregationPendingContributions::Wrapper::operator=(
PrivateAggregationPendingContributions::Wrapper&& other) = default;
PrivateAggregationPendingContributions::Wrapper::~Wrapper() = default;
PrivateAggregationPendingContributions&
PrivateAggregationPendingContributions::Wrapper::GetPendingContributions() {
CHECK(base::FeatureList::IsEnabled(
blink::features::kPrivateAggregationApiErrorReporting));
return std::get<0>(contributions_);
}
std::vector<blink::mojom::AggregatableReportHistogramContribution>&
PrivateAggregationPendingContributions::Wrapper::GetContributionsVector() {
CHECK(!base::FeatureList::IsEnabled(
blink::features::kPrivateAggregationApiErrorReporting));
return std::get<1>(contributions_);
}
} // namespace content