| // Copyright 2021 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/aggregation_service/aggregatable_report_sender.h" |
| |
| #include <stdint.h> |
| |
| #include <memory> |
| #include <string> |
| #include <string_view> |
| |
| #include "base/containers/enum_set.h" |
| #include "base/functional/callback_helpers.h" |
| #include "base/memory/scoped_refptr.h" |
| #include "base/metrics/histogram.h" |
| #include "base/metrics/metrics_hashes.h" |
| #include "base/strings/string_util.h" |
| #include "base/test/bind.h" |
| #include "base/test/metrics/histogram_tester.h" |
| #include "base/test/scoped_feature_list.h" |
| #include "base/test/task_environment.h" |
| #include "components/metrics/dwa/dwa_recorder.h" |
| #include "content/browser/aggregation_service/aggregatable_report.h" |
| #include "content/public/test/browser_task_environment.h" |
| #include "net/base/isolation_info.h" |
| #include "net/base/load_flags.h" |
| #include "services/network/public/cpp/weak_wrapper_shared_url_loader_factory.h" |
| #include "services/network/public/mojom/url_response_head.mojom.h" |
| #include "services/network/test/test_url_loader_factory.h" |
| #include "services/network/test/test_utils.h" |
| #include "testing/gtest/include/gtest/gtest.h" |
| #include "url/gurl.h" |
| |
| namespace content { |
| |
| namespace { |
| using DelayTypeEnumSet = |
| base::EnumSet<AggregatableReportRequest::DelayType, |
| AggregatableReportRequest::DelayType::kMinValue, |
| AggregatableReportRequest::DelayType::kMaxValue>; |
| |
| constexpr std::string_view kExampleURL = "https://a.com/"; |
| |
| base::Value GetExampleContents() { |
| base::Value::Dict contents; |
| contents.Set("id", "1234"); |
| return base::Value(std::move(contents)); |
| } |
| |
| void ExpectHistograms( |
| const base::HistogramTester& histograms, |
| std::optional<AggregatableReportRequest::DelayType> delay_type, |
| AggregatableReportSender::RequestStatus request_status, |
| int http_response_or_net_error) { |
| EXPECT_THAT( |
| metrics::dwa::DwaRecorder::Get()->GetEntriesForTesting(), |
| testing::ElementsAre(testing::Pointee(testing::AllOf( |
| testing::Field( |
| &metrics::dwa::mojom::DwaEntry::event_hash, |
| base::HashMetricName( |
| "PrivacySandbox.AggregationService.ReportSenderAttempt")), |
| |
| // The content is set as the reporting origin, which is based off |
| // `kExampleURL` in our tests. DWA content sanitization extracts the |
| // eTLD+1 from this value, yielding "a.com". |
| testing::Field(&metrics::dwa::mojom::DwaEntry::content_hash, |
| base::HashMetricName("a.com")), |
| testing::Field(&metrics::dwa::mojom::DwaEntry::metrics, |
| testing::UnorderedElementsAre(testing::Pair( |
| base::HashMetricName("Status"), |
| static_cast<int64_t>(request_status)))))))); |
| |
| // Ensures that metrics are only counted since the last call to |
| // `ExpectHistograms()`. |
| // TODO(crbug.com/403946431): Consider implementing a scoped object to improve |
| // ergonomics. |
| metrics::dwa::DwaRecorder::Get()->Purge(); |
| |
| auto GetName = [](auto... pieces) -> std::string { |
| constexpr std::string_view kHistogramPrefix = |
| "PrivacySandbox.AggregationService.ReportSender"; |
| return base::JoinString({kHistogramPrefix, pieces...}, "."); |
| }; |
| |
| // The combined variants of the Status and HttpResponseOrNetErrorCode |
| // histograms should be recorded regardless of `delay_type`. |
| histograms.ExpectUniqueSample(GetName("Status"), request_status, |
| /*expected_bucket_count=*/1); |
| histograms.ExpectUniqueSample(GetName("HttpResponseOrNetErrorCode"), |
| http_response_or_net_error, |
| /*expected_bucket_count=*/1); |
| |
| if (!delay_type.has_value()) { |
| return; |
| } |
| |
| // Only one delay-specific variant should be recorded for the Status and |
| // HttpResponseOrNetErrorCode histograms. |
| const std::string_view delay_type_str = |
| AggregatableReportRequest::DelayTypeToString(*delay_type); |
| |
| histograms.ExpectUniqueSample(GetName(delay_type_str, "Status"), |
| request_status, /*expected_bucket_count=*/1); |
| |
| histograms.ExpectUniqueSample( |
| GetName(delay_type_str, "HttpResponseOrNetErrorCode"), |
| http_response_or_net_error, |
| /*expected_bucket_count=*/1); |
| |
| DelayTypeEnumSet other_delay_types = DelayTypeEnumSet::All(); |
| other_delay_types.Remove(*delay_type); |
| |
| for (const auto other_delay_type : other_delay_types) { |
| const std::string_view other_delay_type_str = |
| AggregatableReportRequest::DelayTypeToString(other_delay_type); |
| histograms.ExpectTotalCount(GetName(other_delay_type_str, "Status"), 0); |
| histograms.ExpectTotalCount( |
| GetName(other_delay_type_str, "HttpResponseOrNetErrorCode"), 0); |
| } |
| } |
| |
| } // namespace |
| |
| class AggregatableReportSenderTest : public testing::Test { |
| public: |
| AggregatableReportSenderTest() |
| : task_environment_(base::test::TaskEnvironment::TimeSource::MOCK_TIME), |
| sender_(AggregatableReportSender::CreateForTesting( |
| base::MakeRefCounted<network::WeakWrapperSharedURLLoaderFactory>( |
| &test_url_loader_factory_))) {} |
| |
| void SetUp() override { |
| metrics::dwa::DwaRecorder::Get()->EnableRecording(); |
| |
| // Remove any earlier metrics in case recording has already been enabled. |
| metrics::dwa::DwaRecorder::Get()->Purge(); |
| ASSERT_THAT(metrics::dwa::DwaRecorder::Get()->GetEntriesForTesting(), |
| testing::IsEmpty()); |
| } |
| |
| protected: |
| content::BrowserTaskEnvironment task_environment_; |
| network::TestURLLoaderFactory test_url_loader_factory_; |
| std::unique_ptr<AggregatableReportSender> sender_; |
| base::test::ScopedFeatureList scoped_feature_list_{metrics::dwa::kDwaFeature}; |
| }; |
| |
| TEST_F(AggregatableReportSenderTest, ReportSent_RequestAttributesSet) { |
| sender_->SendReport( |
| GURL(kExampleURL), GetExampleContents(), |
| AggregatableReportRequest::DelayType::ScheduledWithFullDelay, |
| base::DoNothing()); |
| |
| const network::ResourceRequest* pending_request; |
| EXPECT_TRUE( |
| test_url_loader_factory_.IsPending(kExampleURL, &pending_request)); |
| |
| EXPECT_EQ(pending_request->url, GURL(kExampleURL)); |
| EXPECT_EQ(pending_request->method, net::HttpRequestHeaders::kPostMethod); |
| EXPECT_EQ(pending_request->credentials_mode, |
| network::mojom::CredentialsMode::kOmit); |
| |
| int load_flags = pending_request->load_flags; |
| EXPECT_TRUE(load_flags & net::LOAD_BYPASS_CACHE); |
| EXPECT_TRUE(load_flags & net::LOAD_DISABLE_CACHE); |
| } |
| |
| TEST_F(AggregatableReportSenderTest, ReportSent_IsolationKeyDifferent) { |
| sender_->SendReport( |
| GURL(kExampleURL), GetExampleContents(), |
| AggregatableReportRequest::DelayType::ScheduledWithFullDelay, |
| base::DoNothing()); |
| sender_->SendReport( |
| GURL(kExampleURL), GetExampleContents(), |
| AggregatableReportRequest::DelayType::ScheduledWithFullDelay, |
| base::DoNothing()); |
| |
| EXPECT_EQ(test_url_loader_factory_.NumPending(), 2); |
| |
| const network::ResourceRequest& request1 = |
| test_url_loader_factory_.GetPendingRequest(0)->request; |
| const network::ResourceRequest& request2 = |
| test_url_loader_factory_.GetPendingRequest(1)->request; |
| |
| EXPECT_EQ(request1.trusted_params->isolation_info.request_type(), |
| net::IsolationInfo::RequestType::kOther); |
| EXPECT_EQ(request2.trusted_params->isolation_info.request_type(), |
| net::IsolationInfo::RequestType::kOther); |
| |
| EXPECT_TRUE(request1.trusted_params->isolation_info.network_isolation_key() |
| .IsTransient()); |
| EXPECT_TRUE(request2.trusted_params->isolation_info.network_isolation_key() |
| .IsTransient()); |
| |
| EXPECT_NE(request1.trusted_params->isolation_info.network_isolation_key(), |
| request2.trusted_params->isolation_info.network_isolation_key()); |
| } |
| |
| TEST_F(AggregatableReportSenderTest, ReportSent_UploadDataCorrect) { |
| sender_->SendReport( |
| GURL(kExampleURL), GetExampleContents(), |
| AggregatableReportRequest::DelayType::ScheduledWithFullDelay, |
| base::DoNothing()); |
| |
| const network::ResourceRequest* pending_request; |
| EXPECT_TRUE( |
| test_url_loader_factory_.IsPending(kExampleURL, &pending_request)); |
| EXPECT_EQ(network::GetUploadData(*pending_request), R"({"id":"1234"})"); |
| } |
| |
| TEST_F(AggregatableReportSenderTest, ReportSent_StatusOk) { |
| base::HistogramTester histograms; |
| |
| sender_->SendReport( |
| GURL(kExampleURL), GetExampleContents(), |
| AggregatableReportRequest::DelayType::ScheduledWithFullDelay, |
| base::BindLambdaForTesting( |
| [&](AggregatableReportSender::RequestStatus status) { |
| EXPECT_EQ(status, AggregatableReportSender::RequestStatus::kOk); |
| })); |
| EXPECT_EQ(test_url_loader_factory_.NumPending(), 1); |
| EXPECT_TRUE(test_url_loader_factory_.SimulateResponseForPendingRequest( |
| kExampleURL, "")); |
| EXPECT_EQ(test_url_loader_factory_.NumPending(), 0); |
| |
| ExpectHistograms(histograms, |
| AggregatableReportRequest::DelayType::ScheduledWithFullDelay, |
| AggregatableReportSender::RequestStatus::kOk, net::HTTP_OK); |
| } |
| |
| TEST_F(AggregatableReportSenderTest, SenderDeletedDuringRequest_NoCrash) { |
| sender_->SendReport( |
| GURL(kExampleURL), GetExampleContents(), |
| AggregatableReportRequest::DelayType::ScheduledWithFullDelay, |
| base::DoNothing()); |
| EXPECT_EQ(test_url_loader_factory_.NumPending(), 1); |
| sender_.reset(); |
| EXPECT_FALSE(test_url_loader_factory_.SimulateResponseForPendingRequest( |
| kExampleURL, "")); |
| } |
| |
| TEST_F(AggregatableReportSenderTest, ReportRequestHangs_Timeout) { |
| base::HistogramTester histograms; |
| |
| sender_->SendReport( |
| GURL(kExampleURL), GetExampleContents(), |
| AggregatableReportRequest::DelayType::ScheduledWithFullDelay, |
| base::BindLambdaForTesting( |
| [&](AggregatableReportSender::RequestStatus status) { |
| EXPECT_EQ(status, |
| AggregatableReportSender::RequestStatus::kNetworkError); |
| })); |
| EXPECT_EQ(test_url_loader_factory_.NumPending(), 1); |
| |
| // The request should time out after 30 seconds. |
| task_environment_.FastForwardBy(base::Seconds(30)); |
| |
| EXPECT_EQ(test_url_loader_factory_.NumPending(), 0); |
| |
| ExpectHistograms(histograms, |
| AggregatableReportRequest::DelayType::ScheduledWithFullDelay, |
| AggregatableReportSender::RequestStatus::kNetworkError, |
| net::ERR_TIMED_OUT); |
| } |
| |
| TEST_F(AggregatableReportSenderTest, |
| ReportRequestFailsDueToNetworkChange_Retries) { |
| // Retry fails |
| { |
| base::HistogramTester histograms; |
| |
| sender_->SendReport( |
| GURL(kExampleURL), GetExampleContents(), |
| AggregatableReportRequest::DelayType::ScheduledWithFullDelay, |
| base::BindLambdaForTesting( |
| [&](AggregatableReportSender::RequestStatus status) { |
| EXPECT_EQ(status, |
| AggregatableReportSender::RequestStatus::kNetworkError); |
| })); |
| EXPECT_EQ(test_url_loader_factory_.NumPending(), 1); |
| |
| // Simulate the request failing due to network change. |
| test_url_loader_factory_.SimulateResponseForPendingRequest( |
| GURL(kExampleURL), |
| network::URLLoaderCompletionStatus(net::ERR_NETWORK_CHANGED), |
| network::mojom::URLResponseHead::New(), std::string()); |
| |
| // The sender should automatically retry. |
| EXPECT_EQ(test_url_loader_factory_.NumPending(), 1); |
| |
| // Simulate a second request failure due to network change. |
| test_url_loader_factory_.SimulateResponseForPendingRequest( |
| GURL(kExampleURL), |
| network::URLLoaderCompletionStatus(net::ERR_NETWORK_CHANGED), |
| network::mojom::URLResponseHead::New(), std::string()); |
| |
| // We should not retry again. |
| EXPECT_EQ(test_url_loader_factory_.NumPending(), 0); |
| |
| ExpectHistograms( |
| histograms, |
| AggregatableReportRequest::DelayType::ScheduledWithFullDelay, |
| AggregatableReportSender::RequestStatus::kNetworkError, |
| net::ERR_NETWORK_CHANGED); |
| } |
| |
| // Retry succeeds |
| { |
| base::HistogramTester histograms; |
| |
| sender_->SendReport( |
| GURL(kExampleURL), GetExampleContents(), |
| AggregatableReportRequest::DelayType::ScheduledWithFullDelay, |
| base::BindLambdaForTesting( |
| [&](AggregatableReportSender::RequestStatus status) { |
| EXPECT_EQ(status, AggregatableReportSender::RequestStatus::kOk); |
| })); |
| EXPECT_EQ(test_url_loader_factory_.NumPending(), 1); |
| |
| // Simulate the request failing due to network change. |
| test_url_loader_factory_.SimulateResponseForPendingRequest( |
| GURL(kExampleURL), |
| network::URLLoaderCompletionStatus(net::ERR_NETWORK_CHANGED), |
| network::mojom::URLResponseHead::New(), std::string()); |
| |
| // The sender should automatically retry. |
| EXPECT_EQ(test_url_loader_factory_.NumPending(), 1); |
| |
| // Simulate a second request with respoonse. |
| EXPECT_TRUE(test_url_loader_factory_.SimulateResponseForPendingRequest( |
| kExampleURL, "")); |
| |
| ExpectHistograms( |
| histograms, |
| AggregatableReportRequest::DelayType::ScheduledWithFullDelay, |
| AggregatableReportSender::RequestStatus::kOk, net::HTTP_OK); |
| } |
| } |
| |
| TEST_F(AggregatableReportSenderTest, HttpError_CallbackRuns) { |
| base::HistogramTester histograms; |
| |
| bool callback_run = false; |
| sender_->SendReport( |
| GURL(kExampleURL), GetExampleContents(), |
| AggregatableReportRequest::DelayType::ScheduledWithFullDelay, |
| base::BindLambdaForTesting( |
| [&](AggregatableReportSender::RequestStatus status) { |
| EXPECT_EQ(status, |
| AggregatableReportSender::RequestStatus::kServerError); |
| callback_run = true; |
| })); |
| |
| // We should run the callback even if there is an http error. |
| EXPECT_TRUE(test_url_loader_factory_.SimulateResponseForPendingRequest( |
| kExampleURL, "", net::HTTP_BAD_REQUEST)); |
| |
| EXPECT_TRUE(callback_run); |
| |
| ExpectHistograms(histograms, |
| AggregatableReportRequest::DelayType::ScheduledWithFullDelay, |
| AggregatableReportSender::RequestStatus::kServerError, |
| net::HTTP_BAD_REQUEST); |
| } |
| |
| TEST_F(AggregatableReportSenderTest, ManyReports_AllSentSuccessfully) { |
| GURL url = GURL(kExampleURL); |
| int num_callbacks_run = 0; |
| for (int i = 0; i < 10; i++) { |
| sender_->SendReport( |
| url, GetExampleContents(), |
| AggregatableReportRequest::DelayType::ScheduledWithFullDelay, |
| base::BindLambdaForTesting( |
| [&](AggregatableReportSender::RequestStatus status) { |
| EXPECT_EQ(status, AggregatableReportSender::RequestStatus::kOk); |
| ++num_callbacks_run; |
| })); |
| } |
| |
| EXPECT_EQ(test_url_loader_factory_.NumPending(), 10); |
| |
| for (int i = 0; i < 10; i++) { |
| EXPECT_TRUE(test_url_loader_factory_.SimulateResponseForPendingRequest( |
| kExampleURL, "")); |
| } |
| |
| EXPECT_EQ(num_callbacks_run, 10); |
| EXPECT_EQ(test_url_loader_factory_.NumPending(), 0); |
| } |
| |
| TEST_F(AggregatableReportSenderTest, StatusHistogram_Expected) { |
| static const std::optional<AggregatableReportRequest::DelayType> |
| kDelayTypeValues[] = { |
| std::nullopt, |
| AggregatableReportRequest::DelayType::Unscheduled, |
| AggregatableReportRequest::DelayType::ScheduledWithFullDelay, |
| AggregatableReportRequest::DelayType::ScheduledWithReducedDelay, |
| }; |
| for (const auto delay_type : kDelayTypeValues) { |
| SCOPED_TRACE(testing::Message() |
| << "delay_type = " |
| << (delay_type ? AggregatableReportRequest::DelayTypeToString( |
| *delay_type) |
| : "std::nullopt")); |
| // All OK. |
| { |
| base::HistogramTester histograms; |
| sender_->SendReport(GURL(kExampleURL), GetExampleContents(), delay_type, |
| base::DoNothing()); |
| EXPECT_TRUE(test_url_loader_factory_.SimulateResponseForPendingRequest( |
| kExampleURL, "")); |
| |
| ExpectHistograms(histograms, delay_type, |
| AggregatableReportSender::RequestStatus::kOk, |
| net::HTTP_OK); |
| } |
| |
| // Network error. |
| { |
| base::HistogramTester histograms; |
| sender_->SendReport(GURL(kExampleURL), GetExampleContents(), delay_type, |
| base::DoNothing()); |
| network::URLLoaderCompletionStatus completion_status(net::ERR_FAILED); |
| EXPECT_TRUE(test_url_loader_factory_.SimulateResponseForPendingRequest( |
| GURL(kExampleURL), completion_status, |
| network::mojom::URLResponseHead::New(), std::string())); |
| ExpectHistograms(histograms, delay_type, |
| AggregatableReportSender::RequestStatus::kNetworkError, |
| net::ERR_FAILED); |
| } |
| |
| // Server error. |
| { |
| base::HistogramTester histograms; |
| sender_->SendReport(GURL(kExampleURL), GetExampleContents(), delay_type, |
| base::DoNothing()); |
| EXPECT_TRUE(test_url_loader_factory_.SimulateResponseForPendingRequest( |
| kExampleURL, std::string(), net::HTTP_UNAUTHORIZED)); |
| ExpectHistograms(histograms, delay_type, |
| AggregatableReportSender::RequestStatus::kServerError, |
| net::HTTP_UNAUTHORIZED); |
| } |
| } |
| } |
| |
| } // namespace content |