blob: af383bd126e74fc08b0905a80b579c7957ad3b1e [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/attribution_reporting/aggregatable_attribution_utils.h"
#include <stdint.h>
#include <optional>
#include <string>
#include <tuple>
#include <utility>
#include <vector>
#include "base/test/metrics/histogram_tester.h"
#include "base/time/time.h"
#include "base/values.h"
#include "components/aggregation_service/aggregation_coordinator_utils.h"
#include "components/attribution_reporting/aggregatable_trigger_data.h"
#include "components/attribution_reporting/aggregatable_values.h"
#include "components/attribution_reporting/aggregation_keys.h"
#include "components/attribution_reporting/filters.h"
#include "components/attribution_reporting/source_registration_error.mojom.h"
#include "components/attribution_reporting/source_type.mojom.h"
#include "components/attribution_reporting/suitable_origin.h"
#include "content/browser/aggregation_service/aggregatable_report.h"
#include "content/browser/attribution_reporting/attribution_report.h"
#include "content/browser/attribution_reporting/attribution_test_utils.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "third_party/abseil-cpp/absl/numeric/int128.h"
#include "third_party/blink/public/mojom/aggregation_service/aggregatable_report.mojom.h"
namespace content {
namespace {
using ::attribution_reporting::AggregatableValues;
using ::attribution_reporting::FilterConfig;
using ::attribution_reporting::FilterPair;
using ::attribution_reporting::mojom::SourceType;
using ::blink::mojom::AggregatableReportHistogramContribution;
using ::testing::ElementsAre;
} // namespace
TEST(AggregatableAttributionUtilsTest, CreateAggregatableHistogram) {
base::HistogramTester histograms;
auto source = attribution_reporting::AggregationKeys::FromKeys(
{{"key1", 345}, {"key2", 5}, {"key3", 123}});
ASSERT_TRUE(source.has_value());
base::Time source_time = base::Time::Now();
base::Time trigger_time = source_time + base::Seconds(5);
std::vector<attribution_reporting::AggregatableTriggerData>
aggregatable_trigger_data{
// The first trigger data applies to "key1", "key3".
*attribution_reporting::AggregatableTriggerData::Create(
absl::MakeUint128(/*high=*/0, /*low=*/1024),
/*source_keys=*/{"key1", "key3"},
FilterPair(
/*positive=*/{*FilterConfig::Create({{"filter", {"value"}}})},
/*negative=*/{})),
// The second trigger data applies to "key2", "key4" is ignored.
*attribution_reporting::AggregatableTriggerData::Create(
absl::MakeUint128(/*high=*/0, /*low=*/2688),
/*source_keys=*/{"key2", "key4"},
FilterPair(
/*positive=*/{*FilterConfig::Create({{"a", {"b", "c"}}})},
/*negative=*/{})),
// The third trigger will be ignored due to mismatched filters.
*attribution_reporting::AggregatableTriggerData::Create(
absl::MakeUint128(/*high=*/0, /*low=*/4096),
/*source_keys=*/{"key1", "key2"},
FilterPair(/*positive=*/{*FilterConfig::Create({{"filter", {}}})},
/*negative=*/{})),
// The fourth trigger will be ignored due to matched not_filters.
*attribution_reporting::AggregatableTriggerData::Create(
absl::MakeUint128(/*high=*/0, /*low=*/4096),
/*source_keys=*/{"key1", "key2"},
FilterPair(
/*positive=*/{},
/*negative=*/{*FilterConfig::Create(
{{"filter", {"value"}}})})),
// The fifth trigger will be ignored due to mismatched
// lookback_window.
*attribution_reporting::AggregatableTriggerData::Create(
absl::MakeUint128(/*high=*/0, /*low=*/4096),
/*source_keys=*/{"key1", "key3"},
FilterPair(
/*positive=*/{*FilterConfig::Create(
{{"filter", {"value"}}},
/*lookback_window=*/base::Seconds(5) -
base::Microseconds(1))},
/*negative=*/{})),
};
std::optional<attribution_reporting::FilterData> source_filter_data =
attribution_reporting::FilterData::Create({{"filter", {"value"}}});
ASSERT_TRUE(source_filter_data.has_value());
auto aggregatable_values = *attribution_reporting::AggregatableValues::Create(
{{"key1", 32768}, {"key2", 1664}}, FilterPair());
std::vector<AggregatableReportHistogramContribution> contributions =
CreateAggregatableHistogram(
*source_filter_data, SourceType::kEvent, source_time, trigger_time,
*source, std::move(aggregatable_trigger_data), {aggregatable_values});
// "key3" is not present as no value is found.
EXPECT_THAT(
contributions,
ElementsAre(
AggregatableReportHistogramContribution(
/*bucket=*/1369, /*value=*/32768, /*filtering_id=*/std::nullopt),
AggregatableReportHistogramContribution(
/*bucket=*/2693, /*value=*/1664, /*filtering_id=*/std::nullopt)));
histograms.ExpectUniqueSample(
"Conversions.AggregatableReport.FilteredTriggerDataPercentage", 60, 1);
histograms.ExpectUniqueSample(
"Conversions.AggregatableReport.DroppedKeysPercentage", 33, 1);
histograms.ExpectUniqueSample(
"Conversions.AggregatableReport.NumContributionsPerReport2", 2, 1);
histograms.ExpectUniqueSample(
"Conversions.AggregatableReport.TotalBudgetPerReport", 34432, 1);
}
TEST(AggregatableAttributionUtilsTest,
CreateAggregatableHistogram_ValuesFiltered) {
auto source = attribution_reporting::AggregationKeys::FromKeys(
{{"key1", 345}, {"key2", 5}});
ASSERT_TRUE(source.has_value());
base::Time source_time = base::Time::Now();
base::Time trigger_time = source_time + base::Seconds(5);
std::vector<attribution_reporting::AggregatableTriggerData>
aggregatable_trigger_data{
*attribution_reporting::AggregatableTriggerData::Create(
absl::MakeUint128(/*high=*/0, /*low=*/1024),
/*source_keys=*/{"key1", "key2"}, FilterPair()),
};
attribution_reporting::FilterData source_filter_data =
*attribution_reporting::FilterData::Create({{"product", {"1"}}});
const struct {
const char* description;
std::vector<AggregatableValues> aggregatable_values;
std::vector<AggregatableReportHistogramContribution> expected;
} kTestCases[] =
{{
.description = "filter_not_matching",
.aggregatable_values =
{*attribution_reporting::AggregatableValues::Create(
{{"key1", 32768}}, FilterPair(
/*positive=*/{*FilterConfig::Create(
{{"product", {"2"}}})},
/*negative=*/{}))},
.expected = {},
},
{
.description = "first_entry_skipped",
.aggregatable_values =
{*attribution_reporting::AggregatableValues::Create(
{{"key1", 32768}}, FilterPair(
/*positive=*/{*FilterConfig::Create(
{{"product", {"2"}}})},
/*negative=*/{})),
*attribution_reporting::AggregatableValues::Create(
{{"key2", 1664}}, FilterPair(
/*positive=*/{*FilterConfig::Create(
{{"product", {"1"}}})},
/*negative=*/{}))},
.expected = {AggregatableReportHistogramContribution(
1029, 1664, /*filtering_id=*/std::nullopt)},
},
{
.description = "second_entry_ignored",
.aggregatable_values =
{*attribution_reporting::AggregatableValues::Create(
{{"key1", 32768}}, FilterPair(
/*positive=*/{*FilterConfig::Create(
{{"product", {"1"}}})},
/*negative=*/{})),
*attribution_reporting::AggregatableValues::Create(
{{"key2", 1664}}, FilterPair(
/*positive=*/{*FilterConfig::Create(
{{"product", {"1"}}})},
/*negative=*/{}))},
.expected = {AggregatableReportHistogramContribution(
1369, 32768, /*filtering_id=*/std::nullopt)},
},
{
.description = "filters_matched_keys_mismatched_no_contributions",
.aggregatable_values =
{*attribution_reporting::AggregatableValues::Create(
{{"key3", 32768}}, FilterPair(
/*positive=*/{*FilterConfig::Create(
{{"product", {"1"}}})},
/*negative=*/{})),
// Shouldn't contribute as only the first aggregatable values
// entry with matching filters is considered.
*attribution_reporting::AggregatableValues::Create(
{{"key2", 1664}}, FilterPair(
/*positive=*/{*FilterConfig::Create(
{{"product", {"1"}}})},
/*negative=*/{}))},
.expected = {},
},
{
.description = "not_filter_matching_first_entry_skipped",
.aggregatable_values =
{*attribution_reporting::AggregatableValues::Create(
{{"key1", 32768}},
FilterPair(/*positive=*/{},
/*negative=*/{*FilterConfig::Create(
{{"product", {"1"}}})})),
*attribution_reporting::AggregatableValues::Create(
{{"key2", 1664}}, FilterPair(
/*positive=*/{*FilterConfig::Create(
{{"product", {"1"}}})},
/*negative=*/{}))},
.expected = {AggregatableReportHistogramContribution(
1029, 1664, /*filtering_id=*/std::nullopt)},
}};
for (auto& test_case : kTestCases) {
std::vector<AggregatableReportHistogramContribution> contributions =
CreateAggregatableHistogram(
source_filter_data, SourceType::kEvent, source_time, trigger_time,
*source, aggregatable_trigger_data, test_case.aggregatable_values);
EXPECT_THAT(contributions, test_case.expected) << test_case.description;
}
}
TEST(AggregatableAttributionUtilsTest,
NoTriggerData_FilteredPercentageNotRecorded) {
base::HistogramTester histograms;
auto source =
attribution_reporting::AggregationKeys::FromKeys({{"key1", 345}});
ASSERT_TRUE(source.has_value());
std::vector<AggregatableReportHistogramContribution> contributions =
CreateAggregatableHistogram(
attribution_reporting::FilterData(), SourceType::kNavigation,
/*source_time=*/base::Time::Now(), /*trigger_time=*/base::Time::Now(),
*source,
/*aggregatable_trigger_data=*/{},
/*aggregatable_values=*/
{*attribution_reporting::AggregatableValues::Create({{"key2", 32768}},
FilterPair())});
histograms.ExpectTotalCount(
"Conversions.AggregatableReport.FilteredTriggerDataPercentage", 0);
histograms.ExpectUniqueSample(
"Conversions.AggregatableReport.DroppedKeysPercentage", 100, 1);
histograms.ExpectUniqueSample(
"Conversions.AggregatableReport.NumContributionsPerReport2", 0, 1);
histograms.ExpectUniqueSample(
"Conversions.AggregatableReport.TotalBudgetPerReport", 0, 1);
}
TEST(AggregatableAttributionUtilsTest, RoundsSourceRegistrationTime) {
const struct {
std::string description;
int64_t source_time;
std::string expected_serialized_time;
} kTestCases[] = {
{"14288 * 86400000", 1234483200000, "1234483200"},
{"14288 * 86400000 + 1", 1234483200001, "1234483200"},
{"14288.5 * 86400000 - 1", 1234526399999, "1234483200"},
{"14288.5 * 86400000", 1234526400000, "1234483200"},
{"14288.5 * 86400000 + 1", 1234526400001, "1234483200"},
{"14289 * 86400000 -1", 1234569599999, "1234483200"},
{"14289 * 86400000", 1234569600000, "1234569600"},
};
for (const auto& test_case : kTestCases) {
base::Time source_time =
base::Time::FromMillisecondsSinceUnixEpoch(test_case.source_time);
AttributionReport report =
ReportBuilder(AttributionInfoBuilder().Build(),
SourceBuilder(source_time).BuildStored())
.SetAggregatableHistogramContributions(
{AggregatableReportHistogramContribution(
/*bucket=*/1,
/*value=*/2, /*filtering_id=*/std::nullopt)})
.BuildAggregatableAttribution();
std::optional<AggregatableReportRequest> request =
CreateAggregatableReportRequest(report);
ASSERT_TRUE(request.has_value());
const base::Value::Dict& additional_fields =
request->shared_info().additional_fields;
const std::string* actual_serialized_time =
additional_fields.FindString("source_registration_time");
ASSERT_TRUE(actual_serialized_time);
EXPECT_EQ(*actual_serialized_time, test_case.expected_serialized_time)
<< test_case.description;
}
}
TEST(AggregatableAttributionUtilsTest, AggregationCoordinatorSet) {
auto coordinator_origin = attribution_reporting::SuitableOrigin::Create(
::aggregation_service::GetDefaultAggregationCoordinatorOrigin());
AttributionReport report =
ReportBuilder(AttributionInfoBuilder().Build(),
SourceBuilder().BuildStored())
.SetAggregatableHistogramContributions(
{AggregatableReportHistogramContribution(
/*bucket=*/1,
/*value=*/2, /*filtering_id=*/std::nullopt)})
.SetAggregationCoordinatorOrigin(*coordinator_origin)
.BuildAggregatableAttribution();
std::optional<AggregatableReportRequest> request =
CreateAggregatableReportRequest(report);
ASSERT_TRUE(request.has_value());
EXPECT_EQ(request->payload_contents().aggregation_coordinator_origin,
coordinator_origin);
}
TEST(AggregatableAttributionUtilsTest, AggregatableReportRequestForNullReport) {
std::optional<AggregatableReportRequest> request =
CreateAggregatableReportRequest(
ReportBuilder(
AttributionInfoBuilder().Build(),
SourceBuilder(
base::Time::FromMillisecondsSinceUnixEpoch(1234567890123))
.BuildStored())
.BuildNullAggregatable());
ASSERT_TRUE(request.has_value());
EXPECT_TRUE(request->payload_contents().contributions.empty());
EXPECT_FALSE(
request->payload_contents().aggregation_coordinator_origin.has_value());
const std::string* source_registration_time =
request->shared_info().additional_fields.FindString(
"source_registration_time");
ASSERT_TRUE(source_registration_time);
EXPECT_EQ(*source_registration_time, "1234483200");
}
TEST(AggregatableAttributionUtilsTest,
AggregatableReportRequestExcludingSourceRegistrationTime) {
std::optional<AggregatableReportRequest> request =
CreateAggregatableReportRequest(
ReportBuilder(
AttributionInfoBuilder().Build(),
SourceBuilder(
base::Time::FromMillisecondsSinceUnixEpoch(1234567890123))
.BuildStored())
.SetAggregatableHistogramContributions(
{AggregatableReportHistogramContribution(
/*bucket=*/1,
/*value=*/2, /*filtering_id=*/std::nullopt)})
.SetSourceRegistrationTimeConfig(
attribution_reporting::mojom::SourceRegistrationTimeConfig::
kExclude)
.BuildAggregatableAttribution());
ASSERT_TRUE(request.has_value());
const std::string* source_registration_time =
request->shared_info().additional_fields.FindString(
"source_registration_time");
ASSERT_TRUE(source_registration_time);
EXPECT_EQ(*source_registration_time, "0");
}
TEST(AggregatableAttributionUtilsTest, TotalBudgetMetrics) {
const struct {
const char* desc;
attribution_reporting::AggregationKeys::Keys keys;
AggregatableValues::Values values;
int expected;
} kTestCases[] = {
{
.desc = "within-max",
.keys = {{"a", 1}, {"b", 2}},
.values = {{"a", 1}, {"b", 65535}},
.expected = 65536,
},
{
.desc = "exceed-max",
.keys = {{"a", 1}, {"b", 2}},
.values = {{"a", 10}, {"b", 65536}},
.expected = 100000,
},
};
for (auto& test_case : kTestCases) {
SCOPED_TRACE(test_case.desc);
base::Time now = base::Time::Now();
base::HistogramTester histograms;
std::ignore = CreateAggregatableHistogram(
attribution_reporting::FilterData(), SourceType::kEvent,
/*source_time=*/now,
/*trigger_time=*/now,
*attribution_reporting::AggregationKeys::FromKeys(test_case.keys),
{attribution_reporting::AggregatableTriggerData()},
{*AggregatableValues::Create(test_case.values, FilterPair())});
histograms.ExpectUniqueSample(
"Conversions.AggregatableReport.TotalBudgetPerReport",
test_case.expected, 1);
}
}
} // namespace content