blob: 576952ca21247cfd3d85ae2a45b83d8d41636a33 [file] [log] [blame]
// 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