910e62b5创建于 1月15日历史提交
// 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