910e62b5创建于 1月15日历史提交
// Copyright 2022 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_host.h"

#include <stddef.h>
#include <stdint.h>

#include <algorithm>
#include <bit>
#include <iterator>
#include <map>
#include <memory>
#include <optional>
#include <set>
#include <string>
#include <string_view>
#include <utility>
#include <vector>

#include "base/check.h"
#include "base/check_deref.h"
#include "base/check_op.h"
#include "base/command_line.h"
#include "base/containers/extend.h"
#include "base/containers/flat_map.h"
#include "base/feature_list.h"
#include "base/functional/bind.h"
#include "base/functional/callback.h"
#include "base/location.h"
#include "base/metrics/histogram_functions.h"
#include "base/notreached.h"
#include "base/numerics/clamped_math.h"
#include "base/numerics/safe_conversions.h"
#include "base/rand_util.h"
#include "base/strings/strcat.h"
#include "base/time/time.h"
#include "base/timer/elapsed_timer.h"
#include "base/timer/timer.h"
#include "base/uuid.h"
#include "base/values.h"
#include "components/aggregation_service/aggregation_coordinator_utils.h"
#include "content/browser/aggregation_service/aggregatable_report.h"
#include "content/browser/private_aggregation/private_aggregation_budget_key.h"
#include "content/browser/private_aggregation/private_aggregation_caller_api.h"
#include "content/browser/private_aggregation/private_aggregation_features.h"
#include "content/browser/private_aggregation/private_aggregation_manager.h"
#include "content/browser/private_aggregation/private_aggregation_pending_contributions.h"
#include "content/browser/private_aggregation/private_aggregation_utils.h"
#include "content/public/browser/content_browser_client.h"
#include "content/public/common/content_client.h"
#include "content/public/common/content_switches.h"
#include "mojo/public/cpp/bindings/message.h"
#include "mojo/public/cpp/bindings/pending_receiver.h"
#include "mojo/public/cpp/bindings/receiver_set.h"
#include "services/network/public/cpp/is_potentially_trustworthy.h"
#include "third_party/blink/public/common/features.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"
#include "url/origin.h"

namespace content {

namespace {

void RecordPipeResultHistogram(PrivateAggregationHost::PipeResult result) {
  base::UmaHistogramEnumeration(
      "PrivacySandbox.PrivateAggregation.Host.PipeResult", result);
}

void RecordTimeoutResultHistogram(
    PrivateAggregationHost::TimeoutResult result) {
  base::UmaHistogramEnumeration(
      "PrivacySandbox.PrivateAggregation.Host.TimeoutResult", result);
}

void RecordFilteringIdStatusHistogram(bool has_filtering_id,
                                      bool has_custom_max_bytes) {
  PrivateAggregationHost::FilteringIdStatus status;

  if (has_filtering_id) {
    if (has_custom_max_bytes) {
      status = PrivateAggregationHost::FilteringIdStatus::
          kFilteringIdProvidedWithCustomMaxBytes;
    } else {
      status = PrivateAggregationHost::FilteringIdStatus::
          kFilteringIdProvidedWithDefaultMaxBytes;
    }
  } else {
    if (has_custom_max_bytes) {
      status = PrivateAggregationHost::FilteringIdStatus::
          kNoFilteringIdWithCustomMaxBytes;
    } else {
      status = PrivateAggregationHost::FilteringIdStatus::
          kNoFilteringIdWithDefaultMaxBytes;
    }
  }
  base::UmaHistogramEnumeration(
      "PrivacySandbox.PrivateAggregation.Host.FilteringIdStatus", status);
}

std::vector<std::string_view> GetSuffixesForHistograms(
    PrivateAggregationCallerApi caller_api,
    bool has_timeout) {
  constexpr std::string_view kProtectedAudienceSuffix = ".ProtectedAudience";
  constexpr std::string_view kSharedStorageSuffix = ".SharedStorage";
  constexpr std::string_view kSharedStorageReducedDelaySuffix =
      ".SharedStorage.ReducedDelay";
  constexpr std::string_view kSharedStorageFullDelaySuffix =
      ".SharedStorage.FullDelay";

  switch (caller_api) {
    case PrivateAggregationCallerApi::kProtectedAudience:
      return {kProtectedAudienceSuffix};
    case PrivateAggregationCallerApi::kSharedStorage:
      return {kSharedStorageSuffix, has_timeout
                                        ? kSharedStorageReducedDelaySuffix
                                        : kSharedStorageFullDelaySuffix};
    default:
      NOTREACHED();
  }
}

// `num_merge_keys_sent_or_truncated` is the total number of merge keys (i.e.
// unique bucket and filtering ID pairs) that passed through the mojo pipe.
void RecordNumberOfContributionMergeKeysHistogram(
    size_t num_merge_keys_sent_or_truncated,
    PrivateAggregationCallerApi caller_api,
    bool has_timeout) {
  constexpr std::string_view kMergeKeysHistogramBase =
      "PrivacySandbox.PrivateAggregation.Host.NumContributionMergeKeysInPipe";

  base::UmaHistogramCounts10000(kMergeKeysHistogramBase,
                                num_merge_keys_sent_or_truncated);

  for (std::string_view histogram_suffix :
       GetSuffixesForHistograms(caller_api, has_timeout)) {
    base::UmaHistogramCounts10000(
        base::StrCat({kMergeKeysHistogramBase, histogram_suffix}),
        num_merge_keys_sent_or_truncated);
  }
}

using ContributionMergeKey =
    PrivateAggregationPendingContributions::ContributionMergeKey;

}  // namespace

struct PrivateAggregationHost::ReceiverContext {
  url::Origin worklet_origin;
  url::Origin top_frame_origin;
  PrivateAggregationCallerApi caller_api;
  std::optional<std::string> context_id;
  std::optional<url::Origin> aggregation_coordinator_origin;
  size_t filtering_id_max_bytes;
  size_t effective_max_contributions;
  NullReportBehavior null_report_behavior = NullReportBehavior::kSendNullReport;

  // These fields are only used when `kPrivateAggregationApiErrorReporting` is
  // disabled.
  // TODO(crbug.com/381788013): Remove once feature is fully launched and flag
  // is removed.
  struct {
    // If contributions have been truncated, tracks this for triggering the
    // right histogram value.
    bool did_truncate_contributions = false;

    // Contributions passed to `ContributeToHistogram()` for this receiver,
    // associated with their merge keys.
    std::map<ContributionMergeKey,
             blink::mojom::AggregatableReportHistogramContribution>
        accepted_contributions;
  } pending_contributions_if_error_reporting_disabled;

  // Handles both unconditional and conditional contributions for this receiver.
  // Only populated if `kPrivateAggregationApiErrorReporting` is enabled.
  // TODO(crbug.com/381788013): Remove the optional wrapper once the feature is
  // fully launched and its flag is removed as we will no longer need the
  // Wrapper.
  std::optional<PrivateAggregationPendingContributions>
      pending_contributions_if_error_reporting_enabled;

  // For metrics only. Tracks those dropped due to the contribution limit.
  std::set<ContributionMergeKey> truncated_merge_keys;

  // The debug mode details to use if a non-null report is sent. Cannot be null.
  blink::mojom::DebugModeDetailsPtr report_debug_details =
      blink::mojom::DebugModeDetails::New();

  // If a timeout is specified by the client, this timer will be used to
  // schedule the timeout task. This should be nullptr iff no timeout is
  // specified by the client.
  std::unique_ptr<base::OneShotTimer> timeout_timer;

  // Tracks the duration of time that the mojo pipe has been open. Used for
  // duration measurement to ensure each pipe is being closed appropriately.
  base::ElapsedTimer pipe_duration_timer;
};

PrivateAggregationHost::PrivateAggregationHost(
    base::RepeatingCallback<
        void(ReportRequestGenerator,
             PrivateAggregationPendingContributions::Wrapper,
             PrivateAggregationBudgetKey,
             NullReportBehavior)> on_report_request_details_received,
    BrowserContext* browser_context)
    : should_not_delay_reports_(
          base::CommandLine::ForCurrentProcess()->HasSwitch(
              switches::kPrivateAggregationDeveloperMode)),
      on_report_request_details_received_(
          std::move(on_report_request_details_received)),
      browser_context_(CHECK_DEREF(browser_context)) {
  CHECK(!on_report_request_details_received_.is_null());

  // `base::Unretained()` is safe as `receiver_set_` is owned by `this`.
  receiver_set_.set_disconnect_handler(base::BindRepeating(
      &PrivateAggregationHost::OnReceiverDisconnected, base::Unretained(this)));
}

PrivateAggregationHost::~PrivateAggregationHost() {
  for (const auto& [id, context_ptr] : receiver_set_.GetAllContexts()) {
    ReceiverContext& context = CHECK_DEREF(context_ptr);

    base::UmaHistogramLongTimes(
        "PrivacySandbox.PrivateAggregation.Host.PipeOpenDurationOnShutdown",
        context.pipe_duration_timer.Elapsed());

    if (context.timeout_timer) {
      RecordTimeoutResultHistogram(TimeoutResult::kStillScheduledOnShutdown);
    }
  }
}

// static
base::StrictNumeric<size_t>
PrivateAggregationHost::GetEffectiveMaxContributions(
    PrivateAggregationCallerApi caller_api,
    std::optional<size_t> requested_max_contributions) {
  // These constants define the maximum number of contributions that can go in
  // an `AggregatableReport` after merging.
  static constexpr size_t kMaxContributionsSharedStorage = 20;
  static constexpr size_t kMaxContributionsProtectedAudience = 100;
  static constexpr size_t kMaxContributionsWhenCustomized = 1000;

  if (requested_max_contributions.has_value()) {
    // Calling APIs should not pass the `maxContributions` field through when
    // the feature is disabled.
    CHECK(base::FeatureList::IsEnabled(
        blink::features::kPrivateAggregationApiMaxContributions));
    // Calling APIs must not pass a value of zero.
    CHECK_GT(*requested_max_contributions, 0u);
    return std::min(*requested_max_contributions,
                    kMaxContributionsWhenCustomized);
  }

  switch (caller_api) {
    case PrivateAggregationCallerApi::kSharedStorage:
      return kMaxContributionsSharedStorage;
    case PrivateAggregationCallerApi::kProtectedAudience:
      return kMaxContributionsProtectedAudience;
  }
  NOTREACHED();
}

bool PrivateAggregationHost::BindNewReceiver(
    url::Origin worklet_origin,
    url::Origin top_frame_origin,
    PrivateAggregationCallerApi caller_api,
    std::optional<std::string> context_id,
    std::optional<base::TimeDelta> timeout,
    std::optional<url::Origin> aggregation_coordinator_origin,
    size_t filtering_id_max_bytes,
    std::optional<size_t> max_contributions,
    mojo::PendingReceiver<blink::mojom::PrivateAggregationHost>
        pending_receiver) {
  // If rejected, let the pending receiver be destroyed as it goes out of scope
  // so none of its requests are processed.
  if (!network::IsOriginPotentiallyTrustworthy(worklet_origin)) {
    return false;
  }

  if (context_id.has_value() &&
      context_id.value().size() > kMaxContextIdLength) {
    return false;
  }

  if (aggregation_coordinator_origin.has_value() &&
      !aggregation_service::IsAggregationCoordinatorOriginAllowed(
          aggregation_coordinator_origin.value())) {
    return false;
  }

  if (filtering_id_max_bytes < 1 ||
      filtering_id_max_bytes >
          AggregationServicePayloadContents::kMaximumFilteringIdMaxBytes) {
    return false;
  }

  if (max_contributions.has_value() &&
      !base::FeatureList::IsEnabled(
          blink::features::kPrivateAggregationApiMaxContributions)) {
    return false;
  }

  const bool needs_deterministic_report_count =
      PrivateAggregationManager::ShouldSendReportDeterministically(
          caller_api, context_id, filtering_id_max_bytes, max_contributions);

  // Enforce that reduced delay is used iff null reports are enabled.
  if (timeout.has_value() != needs_deterministic_report_count) {
    return false;
  }

  size_t effective_max_contributions = GetEffectiveMaxContributions(
      caller_api, /*requested_max_contributions=*/max_contributions);

  std::optional<PrivateAggregationPendingContributions>
      pending_contributions_if_error_reporting_enabled;
  if (base::FeatureList::IsEnabled(
          blink::features::kPrivateAggregationApiErrorReporting)) {
    pending_contributions_if_error_reporting_enabled =
        PrivateAggregationPendingContributions(
            effective_max_contributions,
            GetSuffixesForHistograms(caller_api, timeout.has_value()));
  }

  mojo::ReceiverId id = receiver_set_.Add(
      this, std::move(pending_receiver),
      ReceiverContext{
          .worklet_origin = std::move(worklet_origin),
          .top_frame_origin = std::move(top_frame_origin),
          .caller_api = caller_api,
          .context_id = std::move(context_id),
          .aggregation_coordinator_origin =
              std::move(aggregation_coordinator_origin),
          .filtering_id_max_bytes = filtering_id_max_bytes,
          .effective_max_contributions = effective_max_contributions,
          .null_report_behavior = needs_deterministic_report_count
                                      ? NullReportBehavior::kSendNullReport
                                      : NullReportBehavior::kDontSendReport,
          .pending_contributions_if_error_reporting_enabled =
              std::move(pending_contributions_if_error_reporting_enabled),
      });

  if (timeout.has_value()) {
    CHECK(timeout->is_positive());

    ReceiverContext& context = CHECK_DEREF(receiver_set_.GetContext(id));
    context.timeout_timer = std::make_unique<base::OneShotTimer>();
    context.timeout_timer->Start(
        FROM_HERE, *timeout,
        base::BindOnce(
            &PrivateAggregationHost::OnTimeoutBeforeDisconnect,
            // Passing `base::Unretained(this)` is safe as `this` owns the
            // receiver context and the receiver context owns the timer.
            base::Unretained(this), id));
  }

  return true;
}

bool PrivateAggregationHost::IsDebugModeAllowed(
    const url::Origin& top_frame_origin,
    const url::Origin& reporting_origin) {
  if (!blink::features::kPrivateAggregationApiDebugModeEnabledAtAll.Get()) {
    return false;
  }

  if (!base::FeatureList::IsEnabled(
          kPrivateAggregationApiDebugModeRequires3pcEligibility)) {
    return true;
  }

  return GetContentClient()->browser()->IsPrivateAggregationDebugModeAllowed(
      &*browser_context_, top_frame_origin, reporting_origin);
}

bool PrivateAggregationHost::ValidateContributeCall(
    const std::vector<blink::mojom::AggregatableReportHistogramContributionPtr>&
        contribution_ptrs) {
  const url::Origin& reporting_origin =
      receiver_set_.current_context().worklet_origin;
  CHECK(network::IsOriginPotentiallyTrustworthy(reporting_origin));

  if (!GetContentClient()->browser()->IsPrivateAggregationAllowed(
          &*browser_context_, receiver_set_.current_context().top_frame_origin,
          reporting_origin, /*out_block_is_site_setting_specific=*/nullptr)) {
    CloseCurrentPipe(PipeResult::kApiDisabledInSettings);
    return false;
  }

  using ContributionPtr =
      blink::mojom::AggregatableReportHistogramContributionPtr;

  // Null pointers should fail mojo validation.
  CHECK(std::ranges::none_of(contribution_ptrs, &ContributionPtr::is_null));

  if (std::ranges::any_of(contribution_ptrs,
                          [](const ContributionPtr& contribution) {
                            return contribution->value < 0;
                          })) {
    mojo::ReportBadMessage("Negative value encountered");
    CloseCurrentPipe(PipeResult::kNegativeValue);
    return false;
  }

  if (std::ranges::any_of(
          contribution_ptrs, [&](const ContributionPtr& contribution) {
            return static_cast<size_t>(
                       std::bit_width(contribution->filtering_id.value_or(0))) >
                   8 * receiver_set_.current_context().filtering_id_max_bytes;
          })) {
    mojo::ReportBadMessage("Filtering ID too big for max bytes");
    CloseCurrentPipe(PipeResult::kFilteringIdInvalid);
    return false;
  }

  return true;
}

void PrivateAggregationHost::ContributeToHistogram(
    std::vector<blink::mojom::AggregatableReportHistogramContributionPtr>
        contribution_ptrs) {
  if (!ValidateContributeCall(contribution_ptrs)) {
    return;
  }

  if (base::FeatureList::IsEnabled(
          blink::features::kPrivateAggregationApiErrorReporting)) {
    std::vector<blink::mojom::AggregatableReportHistogramContribution>
        contributions;
    base::Extend(
        contributions, std::move(contribution_ptrs),
        &blink::mojom::AggregatableReportHistogramContributionPtr::operator*);

    receiver_set_.current_context()
        .pending_contributions_if_error_reporting_enabled
        ->AddUnconditionalContributions(std::move(contributions));
    return;
  }

  std::map<ContributionMergeKey,
           blink::mojom::AggregatableReportHistogramContribution>&
      accepted_contributions =
          receiver_set_.current_context()
              .pending_contributions_if_error_reporting_disabled
              .accepted_contributions;

  for (blink::mojom::AggregatableReportHistogramContributionPtr& contribution :
       contribution_ptrs) {
    if (contribution->value == 0) {
      // Drop the contribution
      continue;
    }

    ContributionMergeKey merge_key(contribution);

    CHECK_LE(accepted_contributions.size(),
             receiver_set_.current_context().effective_max_contributions);

    auto accepted_contributions_it = accepted_contributions.find(merge_key);

    if (accepted_contributions_it == accepted_contributions.end()) {
      if (accepted_contributions.size() ==
          receiver_set_.current_context().effective_max_contributions) {
        receiver_set_.current_context()
            .pending_contributions_if_error_reporting_disabled
            .did_truncate_contributions = true;

        // Bound worst-case memory usage
        constexpr size_t kMaxTruncatedMergeKeysTracked = 10'000;
        if (receiver_set_.current_context().truncated_merge_keys.size() <
            kMaxTruncatedMergeKeysTracked) {
          receiver_set_.current_context().truncated_merge_keys.insert(
              std::move(merge_key));
        }
        continue;
      }
      accepted_contributions.emplace(std::move(merge_key),
                                     *std::move(contribution));
    } else {
      accepted_contributions_it->second.value =
          base::ClampedNumeric(accepted_contributions_it->second.value) +
          contribution->value;
    }
  }
}

void PrivateAggregationHost::ContributeToHistogramOnEvent(
    blink::mojom::PrivateAggregationErrorEvent error_event,
    std::vector<blink::mojom::AggregatableReportHistogramContributionPtr>
        contribution_ptrs) {
  if (!base::FeatureList::IsEnabled(
          blink::features::kPrivateAggregationApiErrorReporting)) {
    mojo::ReportBadMessage(
        "ContributeToHistogramOnErrorEvent() called when error reporting "
        "feature is disabled");
    CloseCurrentPipe(PipeResult::kNecessaryFeatureNotEnabled);
    return;
  }

  if (!ValidateContributeCall(contribution_ptrs)) {
    return;
  }

  std::vector<blink::mojom::AggregatableReportHistogramContribution>
      contributions;
  base::Extend(
      contributions, std::move(contribution_ptrs),
      &blink::mojom::AggregatableReportHistogramContributionPtr::operator*);

  receiver_set_.current_context()
      .pending_contributions_if_error_reporting_enabled
      ->AddConditionalContributions(error_event, std::move(contributions));
}

AggregatableReportRequest PrivateAggregationHost::GenerateReportRequest(
    base::ElapsedTimer timeout_or_disconnect_timer,
    blink::mojom::DebugModeDetailsPtr debug_mode_details,
    base::Time scheduled_report_time,
    AggregatableReportRequest::DelayType delay_type,
    base::Uuid report_id,
    const url::Origin& reporting_origin,
    PrivateAggregationCallerApi caller_api,
    std::optional<std::string> context_id,
    std::optional<url::Origin> aggregation_coordinator_origin,
    size_t filtering_id_max_bytes,
    size_t max_contributions,
    std::vector<blink::mojom::AggregatableReportHistogramContribution>
        contributions) {
  // When there are zero contributions, we should only reach here if we are
  // sending a report deterministically.
  CHECK(!contributions.empty() ||
        PrivateAggregationManager::ShouldSendReportDeterministically(
            caller_api, context_id, filtering_id_max_bytes, max_contributions));
  CHECK(debug_mode_details);

  RecordFilteringIdStatusHistogram(
      /*has_filtering_id=*/std::ranges::any_of(
          contributions,
          [](blink::mojom::AggregatableReportHistogramContribution&
                 contribution) {
            return contribution.filtering_id.has_value();
          }),
      /*has_custom_max_bytes=*/filtering_id_max_bytes !=
          kDefaultFilteringIdMaxBytes);

  AggregationServicePayloadContents payload_contents(
      AggregationServicePayloadContents::Operation::kHistogram,
      std::move(contributions),
      std::move(aggregation_coordinator_origin),
      /*max_contributions_allowed=*/max_contributions, filtering_id_max_bytes);

  AggregatableReportSharedInfo shared_info(
      scheduled_report_time, std::move(report_id), reporting_origin,
      debug_mode_details->is_enabled
          ? AggregatableReportSharedInfo::DebugMode::kEnabled
          : AggregatableReportSharedInfo::DebugMode::kDisabled,
      /*additional_fields=*/base::Value::Dict(),
      /*api_version=*/kApiReportVersion,
      /*api_identifier=*/
      private_aggregation::GetApiIdentifier(caller_api));

  std::string reporting_path = private_aggregation::GetReportingPath(
      caller_api,
      /*is_immediate_debug_report=*/false);

  std::optional<uint64_t> debug_key;
  if (!debug_mode_details->debug_key.is_null()) {
    CHECK(debug_mode_details->is_enabled);
    debug_key = debug_mode_details->debug_key->value;
  }

  base::flat_map<std::string, std::string> additional_fields;
  if (context_id.has_value()) {
    additional_fields["context_id"] = context_id.value();
  }

  std::optional<AggregatableReportRequest> report_request =
      AggregatableReportRequest::Create(
          std::move(payload_contents), std::move(shared_info), delay_type,
          std::move(reporting_path), debug_key, std::move(additional_fields));

  // All failure cases should've been handled by earlier validation code.
  CHECK(report_request.has_value());

  if (context_id.has_value()) {
    base::UmaHistogramTimes(
        "PrivacySandbox.PrivateAggregation.Host."
        "TimeToGenerateReportRequestWithContextId",
        timeout_or_disconnect_timer.Elapsed());
  }

  return std::move(report_request).value();
}

void PrivateAggregationHost::EnableDebugMode(
    blink::mojom::DebugKeyPtr debug_key) {
  if (receiver_set_.current_context().report_debug_details->is_enabled) {
    mojo::ReportBadMessage("EnableDebugMode() called multiple times");
    CloseCurrentPipe(PipeResult::kEnableDebugModeCalledMultipleTimes);
    return;
  }

  receiver_set_.current_context().report_debug_details->is_enabled = true;
  receiver_set_.current_context().report_debug_details->debug_key =
      std::move(debug_key);
}

void PrivateAggregationHost::CloseCurrentPipe(PipeResult pipe_result) {
  // We should only reach here after an error.
  CHECK_NE(pipe_result, PipeResult::kReportSuccess);
  CHECK_NE(pipe_result,
           PipeResult::kReportSuccessButTruncatedDueToTooManyContributions);
  CHECK_NE(pipe_result, PipeResult::kNoReportButNoError);

  RecordPipeResultHistogram(pipe_result);

  if (receiver_set_.current_context().timeout_timer) {
    CHECK(receiver_set_.current_context().timeout_timer->IsRunning());
    RecordTimeoutResultHistogram(TimeoutResult::kCanceledDueToError);
  }

  mojo::ReceiverId current_receiver = receiver_set_.current_receiver();
  receiver_set_.Remove(current_receiver);
}

void PrivateAggregationHost::OnTimeoutBeforeDisconnect(mojo::ReceiverId id) {
  ReceiverContext& receiver_context = CHECK_DEREF(receiver_set_.GetContext(id));
  SendReportOnTimeoutOrDisconnect(
      receiver_context,
      /*remaining_timeout=*/base::TimeDelta(),
      PrivateAggregationPendingContributions::TimeoutOrDisconnect::kTimeout);

  RecordTimeoutResultHistogram(
      TimeoutResult::kOccurredBeforeRemoteDisconnection);

  receiver_set_.Remove(id);
}

void PrivateAggregationHost::OnReceiverDisconnected() {
  ReceiverContext& current_context = receiver_set_.current_context();
  if (!current_context.timeout_timer) {
    SendReportOnTimeoutOrDisconnect(current_context,
                                    /*remaining_timeout=*/base::TimeDelta(),
                                    PrivateAggregationPendingContributions::
                                        TimeoutOrDisconnect::kDisconnect);
    return;
  }

  CHECK(current_context.timeout_timer->IsRunning());

  RecordTimeoutResultHistogram(
      TimeoutResult::kOccurredAfterRemoteDisconnection);

  // TODO(https://crbug.com/354124875) Add UMA histogram to measure the
  // magnitude of negative `remaining_timeout` values. Also in
  // `OnTimeoutBeforeDisconnect()`.
  base::TimeDelta remaining_timeout =
      current_context.timeout_timer->desired_run_time() -
      base::TimeTicks::Now();

  if (remaining_timeout.is_negative()) {
    remaining_timeout = base::TimeDelta();
  }

  // Speed up tests when developer mode is enabled by ignoring the remaining
  // timeout. See https://crbug.com/362901607#comment7 for context.
  if (should_not_delay_reports_) {
    remaining_timeout = base::TimeDelta();
  }

  SendReportOnTimeoutOrDisconnect(
      current_context, remaining_timeout,
      PrivateAggregationPendingContributions::TimeoutOrDisconnect::kDisconnect);
}

void PrivateAggregationHost::SendReportOnTimeoutOrDisconnect(
    ReceiverContext& receiver_context,
    base::TimeDelta remaining_timeout,
    PrivateAggregationPendingContributions::TimeoutOrDisconnect
        timeout_or_disconnect) {
  CHECK(!remaining_timeout.is_negative());
  base::ElapsedTimer timeout_or_disconnect_timer;

  const url::Origin& reporting_origin = receiver_context.worklet_origin;
  CHECK(network::IsOriginPotentiallyTrustworthy(reporting_origin));

  if (!GetContentClient()->browser()->IsPrivateAggregationAllowed(
          &*browser_context_, receiver_context.top_frame_origin,
          reporting_origin, /*out_block_is_site_setting_specific=*/nullptr)) {
    // No need to remove the pipe from `receiver_set_` as it's already
    // disconnected or will get disconnected synchronously.
    RecordPipeResultHistogram(PipeResult::kApiDisabledInSettings);
    return;
  }

  if (receiver_context.report_debug_details->is_enabled &&
      !IsDebugModeAllowed(receiver_context.top_frame_origin,
                          reporting_origin)) {
    receiver_context.report_debug_details =
        blink::mojom::DebugModeDetails::New();
  }

  std::optional<PrivateAggregationPendingContributions::Wrapper>
      pending_contributions_wrapper;
  bool is_pending_contributions_empty;

  if (base::FeatureList::IsEnabled(
          blink::features::kPrivateAggregationApiErrorReporting)) {
    is_pending_contributions_empty =
        receiver_context.pending_contributions_if_error_reporting_enabled
            ->IsEmpty();

    pending_contributions_wrapper =
        PrivateAggregationPendingContributions::Wrapper(
            std::move(receiver_context
                          .pending_contributions_if_error_reporting_enabled)
                .value());

    pending_contributions_wrapper->GetPendingContributions()
        .MarkContributionsFinalized(timeout_or_disconnect);
  } else {
    std::vector<blink::mojom::AggregatableReportHistogramContribution>
        contributions;

    std::map<ContributionMergeKey,
             blink::mojom::AggregatableReportHistogramContribution>&
        accepted_contributions =
            receiver_context.pending_contributions_if_error_reporting_disabled
                .accepted_contributions;

    RecordNumberOfContributionMergeKeysHistogram(
        accepted_contributions.size() +
            receiver_context.truncated_merge_keys.size(),
        receiver_context.caller_api,
        /*has_timeout=*/!!receiver_context.timeout_timer);

    contributions.reserve(accepted_contributions.size());
    for (auto& contribution_it : accepted_contributions) {
      contributions.push_back(std::move(contribution_it.second));
    }

    is_pending_contributions_empty = contributions.empty();

    pending_contributions_wrapper =
        PrivateAggregationPendingContributions::Wrapper(
            std::move(contributions));
  }

  if (is_pending_contributions_empty) {
    switch (receiver_context.null_report_behavior) {
      case NullReportBehavior::kDontSendReport:
        RecordPipeResultHistogram(PipeResult::kNoReportButNoError);
        return;

      case NullReportBehavior::kSendNullReport:
        // Null reports caused by no contributions don't have debug mode
        // enabled if `kPrivateAggregationApiErrorReporting` is disabled.
        if (!base::FeatureList::IsEnabled(
                blink::features::kPrivateAggregationApiErrorReporting)) {
          receiver_context.report_debug_details =
              blink::mojom::DebugModeDetails::New();
        }
        break;
    }
  }

  const base::Time now = base::Time::Now();

  // If the timeout hasn't been reached, use a modified report issued time.
  base::Time scheduled_report_time = now + remaining_timeout;

  // Add a tiny window to account for local processing time, the majority of
  // which we expect to be spent in `PrivateAggregationBudgeter`. Otherwise, the
  // report time could passively leak information about the previous budgeting
  // history. For context, see <https://crbug.com/324314568>.
  scheduled_report_time += kTimeForLocalProcessing;

  const bool use_reduced_delay =
      should_not_delay_reports_ || receiver_context.timeout_timer;

  if (!use_reduced_delay) {
    // Add a full delay to the report time. The full delay is picked uniformly
    // at random from the range [10 minutes, 1 hour).
    // TODO(alexmt): Consider making this configurable for easier testing.
    scheduled_report_time +=
        base::Minutes(10) + base::RandDouble() * base::Minutes(50);
  }

  const AggregatableReportRequest::DelayType delay_type =
      use_reduced_delay
          ? AggregatableReportRequest::DelayType::ScheduledWithReducedDelay
          : AggregatableReportRequest::DelayType::ScheduledWithFullDelay;

  ReportRequestGenerator report_request_generator = base::BindOnce(
      GenerateReportRequest, std::move(timeout_or_disconnect_timer),
      std::move(receiver_context.report_debug_details), scheduled_report_time,
      delay_type, /*report_id=*/base::Uuid::GenerateRandomV4(),
      reporting_origin, receiver_context.caller_api,
      std::move(receiver_context.context_id),
      std::move(receiver_context.aggregation_coordinator_origin),
      receiver_context.filtering_id_max_bytes,
      receiver_context.effective_max_contributions);

  // Note: `kReportSuccessButTruncatedDueToTooManyContributions` is never
  // recorded if `kPrivateAggregationApiErrorReporting` is enabled as truncation
  // does not occur until later.
  RecordPipeResultHistogram(
      !base::FeatureList::IsEnabled(
          blink::features::kPrivateAggregationApiErrorReporting) &&
              receiver_context.pending_contributions_if_error_reporting_disabled
                  .did_truncate_contributions
          ? PipeResult::kReportSuccessButTruncatedDueToTooManyContributions
          : PipeResult::kReportSuccess);

  std::optional<PrivateAggregationBudgetKey> budget_key =
      PrivateAggregationBudgetKey::Create(
          /*origin=*/reporting_origin, /*api_invocation_time=*/now,
          receiver_context.caller_api);

  // The origin should be potentially trustworthy.
  CHECK(budget_key.has_value());

  on_report_request_details_received_.Run(
      std::move(report_request_generator),
      std::move(*pending_contributions_wrapper), std::move(budget_key.value()),
      receiver_context.null_report_behavior);
}

}  // namespace content