| // 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/attribution_reporting/aggregatable_attribution_utils.h" |
| |
| #include <stddef.h> |
| #include <stdint.h> |
| |
| #include <algorithm> |
| #include <iterator> |
| #include <optional> |
| #include <string> |
| #include <utility> |
| #include <variant> |
| #include <vector> |
| |
| #include "base/check.h" |
| #include "base/containers/flat_tree.h" |
| #include "base/metrics/histogram_functions.h" |
| #include "base/numerics/checked_math.h" |
| #include "base/numerics/clamped_math.h" |
| #include "base/numerics/safe_conversions.h" |
| #include "base/strings/string_number_conversions.h" |
| #include "base/time/time.h" |
| #include "base/values.h" |
| #include "components/attribution_reporting/aggregatable_named_budget_defs.h" |
| #include "components/attribution_reporting/aggregatable_trigger_config.h" |
| #include "components/attribution_reporting/aggregatable_trigger_data.h" |
| #include "components/attribution_reporting/aggregatable_utils.h" |
| #include "components/attribution_reporting/aggregatable_values.h" |
| #include "components/attribution_reporting/aggregation_keys.h" |
| #include "components/attribution_reporting/constants.h" |
| #include "components/attribution_reporting/filters.h" |
| #include "components/attribution_reporting/source_registration_time_config.mojom.h" |
| #include "components/attribution_reporting/source_type.mojom-forward.h" |
| #include "components/attribution_reporting/suitable_origin.h" |
| #include "content/browser/aggregation_service/aggregatable_report.h" |
| #include "content/browser/attribution_reporting/aggregatable_named_budget_pair.h" |
| #include "content/browser/attribution_reporting/attribution_info.h" |
| #include "content/browser/attribution_reporting/attribution_report.h" |
| #include "content/browser/attribution_reporting/stored_source.h" |
| #include "net/base/schemeful_site.h" |
| #include "third_party/blink/public/mojom/aggregation_service/aggregatable_report.mojom.h" |
| |
| namespace content { |
| |
| namespace { |
| |
| // Note: use the same time serialization as in aggregatable_report.cc. |
| // Consider sharing logic if more call-sites need this. |
| std::string SerializeTimeRoundedDownToWholeDayInSeconds(base::Time time) { |
| // TODO(csharrison, linnan): Validate that `time` is valid (e.g. not null / |
| // inf). |
| base::Time rounded = |
| attribution_reporting::RoundDownToWholeDaySinceUnixEpoch(time); |
| return base::NumberToString(rounded.InMillisecondsSinceUnixEpoch() / |
| base::Time::kMillisecondsPerSecond); |
| } |
| |
| } // namespace |
| |
| std::vector<blink::mojom::AggregatableReportHistogramContribution> |
| CreateAggregatableHistogram( |
| const attribution_reporting::FilterData& source_filter_data, |
| attribution_reporting::mojom::SourceType source_type, |
| base::Time source_time, |
| base::Time trigger_time, |
| const attribution_reporting::AggregationKeys& keys, |
| const std::vector<attribution_reporting::AggregatableTriggerData>& |
| aggregatable_trigger_data, |
| const std::vector<attribution_reporting::AggregatableValues>& |
| aggregatable_values) { |
| size_t num_trigger_data_filtered = 0; |
| |
| attribution_reporting::AggregationKeys::Keys buckets = keys.keys(); |
| |
| // For each piece of trigger data specified, check if its filters/not_filters |
| // match for the given source, and if applicable modify the bucket based on |
| // the given key piece. |
| for (const auto& data : aggregatable_trigger_data) { |
| if (!source_filter_data.Matches(source_type, source_time, trigger_time, |
| data.filters())) { |
| ++num_trigger_data_filtered; |
| continue; |
| } |
| |
| for (const auto& source_key : data.source_keys()) { |
| auto bucket = buckets.find(source_key); |
| if (bucket == buckets.end()) { |
| continue; |
| } |
| |
| bucket->second |= data.key_piece(); |
| } |
| } |
| |
| std::vector<blink::mojom::AggregatableReportHistogramContribution> |
| contributions; |
| for (const auto& aggregatable_value : aggregatable_values) { |
| if (source_filter_data.Matches(source_type, source_time, trigger_time, |
| aggregatable_value.filters())) { |
| const attribution_reporting::AggregatableValues::Values& values = |
| aggregatable_value.values(); |
| for (const auto& [key_id, key] : buckets) { |
| auto value = values.find(key_id); |
| if (value == values.end()) { |
| continue; |
| } |
| |
| contributions.emplace_back( |
| key, base::checked_cast<int32_t>(value->second.value()), |
| value->second.filtering_id()); |
| } |
| break; |
| } |
| } |
| |
| if (!aggregatable_trigger_data.empty()) { |
| base::ClampedNumeric<size_t> percentage = num_trigger_data_filtered; |
| percentage *= 100; |
| percentage /= aggregatable_trigger_data.size(); |
| |
| base::UmaHistogramPercentage( |
| "Conversions.AggregatableReport.FilteredTriggerDataPercentage", |
| percentage); |
| } |
| |
| if (!buckets.empty()) { |
| base::ClampedNumeric<size_t> percentage = buckets.size(); |
| percentage -= contributions.size(); |
| percentage *= 100; |
| percentage /= buckets.size(); |
| |
| base::UmaHistogramPercentage( |
| "Conversions.AggregatableReport.DroppedKeysPercentage", percentage); |
| } |
| |
| static_assert(attribution_reporting::kMaxAggregationKeysPerSource == 20, |
| "Bump the version for histogram " |
| "Conversions.AggregatableReport.NumContributionsPerReport2"); |
| |
| base::UmaHistogramExactLinear( |
| "Conversions.AggregatableReport.NumContributionsPerReport2", |
| base::saturated_cast<int>(contributions.size()), |
| attribution_reporting::kMaxAggregationKeysPerSource + 1); |
| |
| // If total values exceeds the max, log the metrics as 100,000 to measure |
| // how often the max is exceeded. |
| static_assert(attribution_reporting::kMaxAggregatableValue == 65536); |
| const int64_t max_value = attribution_reporting::kMaxAggregatableValue + 1; |
| int64_t adjusted_value = std::min( |
| base::MakeStrictNum(max_value), |
| GetTotalAggregatableValues(contributions).ValueOrDefault(max_value)); |
| base::UmaHistogramCounts100000( |
| "Conversions.AggregatableReport.TotalBudgetPerReport", |
| adjusted_value == max_value ? 100000 |
| : base::saturated_cast<int>(adjusted_value)); |
| |
| return contributions; |
| } |
| |
| std::optional<AggregatableReportRequest> CreateAggregatableReportRequest( |
| const AttributionReport& report) { |
| const auto* aggregatable_data = |
| std::get_if<AttributionReport::AggregatableData>(&report.data()); |
| CHECK(aggregatable_data); |
| |
| const AttributionInfo& attribution_info = report.attribution_info(); |
| |
| AggregatableReportSharedInfo::DebugMode debug_mode = |
| report.CanDebuggingBeEnabled() |
| ? AggregatableReportSharedInfo::DebugMode::kEnabled |
| : AggregatableReportSharedInfo::DebugMode::kDisabled; |
| |
| base::Value::Dict additional_fields; |
| switch (aggregatable_data->aggregatable_trigger_config() |
| .source_registration_time_config()) { |
| case attribution_reporting::mojom::SourceRegistrationTimeConfig::kInclude: |
| additional_fields.Set("source_registration_time", |
| SerializeTimeRoundedDownToWholeDayInSeconds( |
| aggregatable_data->source_time())); |
| break; |
| case attribution_reporting::mojom::SourceRegistrationTimeConfig::kExclude: |
| break; |
| } |
| |
| SetAttributionDestination( |
| additional_fields, net::SchemefulSite(attribution_info.context_origin)); |
| |
| return AggregatableReportRequest::Create( |
| AggregationServicePayloadContents( |
| AggregationServicePayloadContents::Operation::kHistogram, |
| aggregatable_data->contributions(), |
| aggregatable_data->aggregation_coordinator_origin() |
| ? std::make_optional( |
| **aggregatable_data->aggregation_coordinator_origin()) |
| : std::nullopt, |
| /*max_contributions_allowed=*/ |
| attribution_reporting::kMaxAggregationKeysPerSource, |
| aggregatable_data->aggregatable_trigger_config() |
| .aggregatable_filtering_id_max_bytes() |
| .value()), |
| AggregatableReportSharedInfo( |
| report.initial_report_time(), report.external_report_id(), |
| report.reporting_origin(), debug_mode, std::move(additional_fields), |
| AttributionReport::AggregatableData::kVersion, |
| AttributionReport::AggregatableData::kApiIdentifier), |
| // The returned request cannot be serialized due to the null `delay_type`. |
| /*delay_type=*/std::nullopt); |
| } |
| |
| base::CheckedNumeric<int64_t> GetTotalAggregatableValues( |
| const std::vector<blink::mojom::AggregatableReportHistogramContribution>& |
| contributions) { |
| base::CheckedNumeric<int64_t> total_value = 0; |
| for (const blink::mojom::AggregatableReportHistogramContribution& |
| contribution : contributions) { |
| total_value += contribution.value; |
| } |
| return total_value; |
| } |
| |
| void SetAttributionDestination(base::Value::Dict& dict, |
| const net::SchemefulSite& destination) { |
| dict.Set("attribution_destination", destination.Serialize()); |
| } |
| |
| StoredSource::AggregatableNamedBudgets ConvertNamedBudgetsMap( |
| const attribution_reporting::AggregatableNamedBudgetDefs& reg_budgets) { |
| StoredSource::AggregatableNamedBudgets::container_type named_budgets; |
| const auto& reg_budget_map = reg_budgets.budgets(); |
| named_budgets.reserve(reg_budget_map.size()); |
| |
| for (const auto& [name, original_budget] : reg_budget_map) { |
| // Budget already validated from parsing. |
| auto budget_pair = AggregatableNamedBudgetPair::Create( |
| original_budget, /*remaining_budget=*/original_budget); |
| CHECK(budget_pair.has_value()); |
| named_budgets.emplace_back(name, *std::move(budget_pair)); |
| }; |
| return StoredSource::AggregatableNamedBudgets(base::sorted_unique, |
| std::move(named_budgets)); |
| } |
| |
| } // namespace content |