blob: e760e6e261aec1eee860ac699ac609ce22bdcf9d [file] [log] [blame]
// Copyright 2025 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "chrome/browser/loader/keep_alive_request_tracker.h"
#include <set>
#include <string>
#include <tuple>
#include <vector>
#include "base/strings/strcat.h"
#include "base/test/scoped_feature_list.h"
#include "base/time/time.h"
#include "base/unguessable_token.h"
#include "components/page_load_metrics/browser/features.h"
#include "components/ukm/test_ukm_recorder.h"
#include "content/public/test/browser_task_environment.h"
#include "content/public/test/keep_alive_url_loader_utils.h"
#include "net/http/http_response_headers.h"
#include "services/metrics/public/cpp/ukm_source_id.h"
#include "services/network/public/cpp/resource_request.h"
#include "services/network/public/cpp/url_loader_completion_status.h"
#include "services/network/public/mojom/attribution.mojom.h"
#include "services/network/public/mojom/url_response_head.mojom.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
namespace {
using RequestStageType = ChromeKeepAliveRequestTracker::RequestStageType;
using RequestType = ChromeKeepAliveRequestTracker::RequestType;
using testing::IsNull;
using testing::NotNull;
constexpr char kRequestCategoryPrefix[] = "test-prefix";
constexpr char kTestUrl[] = "https://example.com";
std::string GetUrlWithCategory(std::string_view category) {
return base::StrCat({kTestUrl, "?category=", category});
}
network::URLLoaderCompletionStatus CreateCompletionStatus(
int error_code,
int extended_error_code) {
network::URLLoaderCompletionStatus status{error_code};
status.extended_error_code = extended_error_code;
return status;
}
MATCHER_P(IsRequestTrackerCreated,
expected,
"ChromeKeepAliveRequestTracker is created") {
return expected ? arg != nullptr : arg == nullptr;
}
class ChromeKeepAliveRequestTrackerTestBase : public testing::Test {
public:
using FeaturesType = std::vector<base::test::FeatureRefAndParams>;
ChromeKeepAliveRequestTrackerTestBase() {
static const FeaturesType enabled_features = {
{page_load_metrics::features::kBeaconLeakageLogging,
{{"category_prefix", kRequestCategoryPrefix}}}};
scoped_feature_list_.InitWithFeaturesAndParameters(enabled_features, {});
}
~ChromeKeepAliveRequestTrackerTestBase() override = default;
protected:
network::ResourceRequest CreateRequest(std::string_view url) {
network::ResourceRequest request;
request.url = GURL(url);
request.attribution_reporting_eligibility =
network::mojom::AttributionReportingEligibility::kEmpty;
request.keepalive = true;
request.keepalive_token = base::UnguessableToken::Create();
request.is_fetch_later_api = IsFetchLaterRequest();
request.attribution_reporting_eligibility =
IsAttributionReportingEligibleRequest()
? network::mojom::AttributionReportingEligibility::kTrigger
: network::mojom::AttributionReportingEligibility::kUnset;
return request;
}
std::unique_ptr<ChromeKeepAliveRequestTracker> CreateTracker(
const network::ResourceRequest& request) const {
return ChromeKeepAliveRequestTracker::MaybeCreateKeepAliveRequestTracker(
request, GetUkmSourceId(),
/*is_context_detached_callback=*/base::BindRepeating([]() {
return false;
}));
}
virtual ukm::SourceId GetUkmSourceId() const {
return ukm::AssignNewSourceId();
}
virtual bool IsFetchLaterRequest() const { return false; }
virtual bool IsAttributionReportingEligibleRequest() const { return false; }
private:
base::test::ScopedFeatureList scoped_feature_list_;
};
// A type to support parameterized testing for the category of the request.
struct CategoryTestCase {
std::string test_case;
std::string category;
bool expected;
};
// A type to support parameterized testing for the request type of the request.
struct RequestTypeTestCase {
std::string test_case;
RequestType request_type;
};
const RequestTypeTestCase kRequestTypeTestCases[] = {
{"Fetch", RequestType::kFetch},
{"Attribution", RequestType::kAttribution},
{"FetchLater", RequestType::kFetchLater},
};
class MaybeCreateKeepAliveRequestTrackerForCategoryTest
: public ChromeKeepAliveRequestTrackerTestBase,
public testing::WithParamInterface<
std::tuple<CategoryTestCase, RequestTypeTestCase>> {
protected:
// ChromeKeepAliveRequestTrackerTestBase overrides:
bool IsFetchLaterRequest() const override {
return std::get<1>(GetParam()).request_type == RequestType::kFetchLater;
}
bool IsAttributionReportingEligibleRequest() const override {
return std::get<1>(GetParam()).request_type == RequestType::kAttribution;
}
};
INSTANTIATE_TEST_SUITE_P(
All,
MaybeCreateKeepAliveRequestTrackerForCategoryTest,
testing::Combine(testing::ValuesIn<CategoryTestCase>({
{"EmptyCategory", "", false},
{"OutOfRangeCategory", "out-of-range-category", false},
{"ValidCategory1", "test-prefix1", true},
{"ValidCategory2", "test-prefix200", true},
}),
testing::ValuesIn(kRequestTypeTestCases)),
[](const testing::TestParamInfo<
std::tuple<CategoryTestCase, RequestTypeTestCase>>& info) {
return std::get<0>(info.param).test_case + "_" +
std::get<1>(info.param).test_case;
});
TEST_P(MaybeCreateKeepAliveRequestTrackerForCategoryTest, WithCategory) {
auto url_category = std::get<0>(GetParam()).category;
auto request = CreateRequest(GetUrlWithCategory(url_category));
auto tracker = CreateTracker(request);
EXPECT_THAT(tracker,
IsRequestTrackerCreated(std::get<0>(GetParam()).expected));
}
class ChromeKeepAliveRequestTrackerTest
: public ChromeKeepAliveRequestTrackerTestBase,
public content::KeepAliveRequestUkmMatcher,
public testing::WithParamInterface<RequestTypeTestCase> {
protected:
ChromeKeepAliveRequestTrackerTest()
: task_environment_(base::test::TaskEnvironment::TimeSource::MOCK_TIME) {}
void SetUp() override {
ukm_recorder_ = std::make_unique<ukm::TestAutoSetUkmRecorder>();
}
void TearDown() override { ukm_recorder_.reset(); }
void FastForwardBy(const base::TimeDelta& delta) {
task_environment_.FastForwardBy(delta);
}
// A small time step useful for testing the passage of time.
static base::TimeDelta one_time_step() { return base::Seconds(1) / 15; }
// KeepAliveRequestUkmMatcher overrides:
ukm::TestAutoSetUkmRecorder& ukm_recorder() override {
return *ukm_recorder_;
}
// ChromeKeepAliveRequestTrackerTestBase overrides:
ukm::SourceId GetUkmSourceId() const override {
return ukm_recorder_->GetNewSourceID();
}
bool IsFetchLaterRequest() const override {
return GetParam().request_type == RequestType::kFetchLater;
}
bool IsAttributionReportingEligibleRequest() const override {
return GetParam().request_type == RequestType::kAttribution;
}
private:
content::BrowserTaskEnvironment task_environment_;
std::unique_ptr<ukm::TestAutoSetUkmRecorder> ukm_recorder_;
};
INSTANTIATE_TEST_SUITE_P(
All,
ChromeKeepAliveRequestTrackerTest,
testing::ValuesIn(kRequestTypeTestCases),
[](const testing::TestParamInfo<RequestTypeTestCase>& info) {
return info.param.test_case;
});
TEST_P(ChromeKeepAliveRequestTrackerTest, NotLogForNonValidCategoryRequest) {
auto request = CreateRequest(kTestUrl);
{
auto tracker = CreateTracker(request);
ASSERT_THAT(tracker, IsNull());
}
ExpectNoUkm();
}
TEST_P(ChromeKeepAliveRequestTrackerTest, LogUkmInDestructor) {
auto request = CreateRequest(GetUrlWithCategory("test-prefix10"));
{
auto tracker = CreateTracker(request);
ASSERT_THAT(tracker, NotNull());
}
ExpectCommonUkm(GetParam().request_type,
/*category_id=*/10,
/*num_redirects=*/0,
/*num_retries=*/0,
/*is_context_detached=*/false,
RequestStageType::kLoaderCreated,
/*previous_stage=*/std::nullopt, *request.keepalive_token);
ExpectTimeSortedTimeDeltaUkm({"TimeDelta.EventLogged"});
}
TEST_P(ChromeKeepAliveRequestTrackerTest, RequestStarted) {
auto request = CreateRequest(GetUrlWithCategory("test-prefix20"));
{
auto tracker = CreateTracker(request);
ASSERT_THAT(tracker, NotNull());
FastForwardBy(one_time_step());
tracker->AdvanceToNextStage(RequestStageType::kRequestStarted);
}
ExpectCommonUkm(GetParam().request_type,
/*category_id=*/20,
/*num_redirects=*/0,
/*num_retries=*/0,
/*is_context_detached=*/false,
RequestStageType::kRequestStarted,
RequestStageType::kLoaderCreated, *request.keepalive_token);
ExpectTimeSortedTimeDeltaUkm(
{"TimeDelta.RequestStarted", "TimeDelta.EventLogged"});
}
TEST_P(ChromeKeepAliveRequestTrackerTest, OneRedirect) {
auto request = CreateRequest(GetUrlWithCategory("test-prefix30"));
{
auto tracker = CreateTracker(request);
ASSERT_THAT(tracker, NotNull());
FastForwardBy(one_time_step());
tracker->AdvanceToNextStage(RequestStageType::kRequestStarted);
FastForwardBy(one_time_step());
tracker->AdvanceToNextStage(RequestStageType::kFirstRedirectReceived);
}
ExpectCommonUkm(GetParam().request_type,
/*category_id=*/30,
/*num_redirects=*/1,
/*num_retries=*/0,
/*is_context_detached=*/false,
RequestStageType::kFirstRedirectReceived,
RequestStageType::kRequestStarted, *request.keepalive_token);
ExpectTimeSortedTimeDeltaUkm({"TimeDelta.RequestStarted",
"TimeDelta.FirstRedirectReceived",
"TimeDelta.EventLogged"});
}
TEST_P(ChromeKeepAliveRequestTrackerTest, TwoRedirects) {
auto request = CreateRequest(GetUrlWithCategory("test-prefix40"));
{
auto tracker = CreateTracker(request);
ASSERT_THAT(tracker, NotNull());
FastForwardBy(one_time_step());
tracker->AdvanceToNextStage(RequestStageType::kRequestStarted);
FastForwardBy(one_time_step());
tracker->AdvanceToNextStage(RequestStageType::kFirstRedirectReceived);
FastForwardBy(one_time_step());
tracker->AdvanceToNextStage(RequestStageType::kSecondRedirectReceived);
}
ExpectCommonUkm(
GetParam().request_type,
/*category_id=*/40,
/*num_redirects=*/2,
/*num_retries=*/0,
/*is_context_detached=*/false, RequestStageType::kSecondRedirectReceived,
RequestStageType::kFirstRedirectReceived, *request.keepalive_token);
ExpectTimeSortedTimeDeltaUkm(
{"TimeDelta.RequestStarted", "TimeDelta.FirstRedirectReceived",
"TimeDelta.SecondRedirectReceived", "TimeDelta.EventLogged"});
}
TEST_P(ChromeKeepAliveRequestTrackerTest, ThreeRedirects) {
auto request = CreateRequest(GetUrlWithCategory("test-prefix50"));
{
auto tracker = CreateTracker(request);
ASSERT_THAT(tracker, NotNull());
FastForwardBy(one_time_step());
tracker->AdvanceToNextStage(RequestStageType::kRequestStarted);
FastForwardBy(one_time_step());
tracker->AdvanceToNextStage(RequestStageType::kFirstRedirectReceived);
FastForwardBy(one_time_step());
tracker->AdvanceToNextStage(RequestStageType::kSecondRedirectReceived);
FastForwardBy(one_time_step());
tracker->AdvanceToNextStage(
RequestStageType::kThirdOrLaterRedirectReceived);
}
ExpectCommonUkm(GetParam().request_type,
/*category_id=*/50,
/*num_redirects=*/3,
/*num_retries=*/0,
/*is_context_detached=*/false,
RequestStageType::kThirdOrLaterRedirectReceived,
RequestStageType::kSecondRedirectReceived,
*request.keepalive_token);
ExpectTimeSortedTimeDeltaUkm(
{"TimeDelta.RequestStarted", "TimeDelta.FirstRedirectReceived",
"TimeDelta.SecondRedirectReceived",
"TimeDelta.ThirdOrLaterRedirectReceived", "TimeDelta.EventLogged"});
}
TEST_P(ChromeKeepAliveRequestTrackerTest, ResponseReceivedAfterRequestStarted) {
auto request = CreateRequest(GetUrlWithCategory("test-prefix60"));
{
auto tracker = CreateTracker(request);
ASSERT_THAT(tracker, NotNull());
FastForwardBy(one_time_step());
tracker->AdvanceToNextStage(RequestStageType::kRequestStarted);
FastForwardBy(one_time_step());
tracker->AdvanceToNextStage(RequestStageType::kResponseReceived);
}
ExpectCommonUkm(GetParam().request_type,
/*category_id=*/60,
/*num_redirects=*/0,
/*num_retries=*/0,
/*is_context_detached=*/false,
RequestStageType::kResponseReceived,
RequestStageType::kRequestStarted, *request.keepalive_token);
ExpectTimeSortedTimeDeltaUkm({"TimeDelta.RequestStarted",
"TimeDelta.ResponseReceived",
"TimeDelta.EventLogged"});
}
TEST_P(ChromeKeepAliveRequestTrackerTest, ResponseReceivedAfterTwoRedirects) {
auto request = CreateRequest(GetUrlWithCategory("test-prefix70"));
{
auto tracker = CreateTracker(request);
ASSERT_THAT(tracker, NotNull());
FastForwardBy(one_time_step());
tracker->AdvanceToNextStage(RequestStageType::kRequestStarted);
FastForwardBy(one_time_step());
tracker->AdvanceToNextStage(RequestStageType::kFirstRedirectReceived);
FastForwardBy(one_time_step());
tracker->AdvanceToNextStage(RequestStageType::kSecondRedirectReceived);
FastForwardBy(one_time_step());
tracker->AdvanceToNextStage(RequestStageType::kResponseReceived);
}
ExpectCommonUkm(
GetParam().request_type,
/*category_id=*/70,
/*num_redirects=*/2,
/*num_retries=*/0,
/*is_context_detached=*/false, RequestStageType::kResponseReceived,
RequestStageType::kSecondRedirectReceived, *request.keepalive_token);
ExpectTimeSortedTimeDeltaUkm(
{"TimeDelta.RequestStarted", "TimeDelta.FirstRedirectReceived",
"TimeDelta.SecondRedirectReceived", "TimeDelta.ResponseReceived",
"TimeDelta.EventLogged"});
}
TEST_P(ChromeKeepAliveRequestTrackerTest, RequestFailedAfterRequestStarted) {
auto request = CreateRequest(GetUrlWithCategory("test-prefix80"));
auto failed_status =
CreateCompletionStatus(/*error_code=*/25, /*extended_error_code=*/1);
{
auto tracker = CreateTracker(request);
ASSERT_THAT(tracker, NotNull());
FastForwardBy(one_time_step());
tracker->AdvanceToNextStage(RequestStageType::kRequestStarted);
FastForwardBy(one_time_step());
tracker->AdvanceToNextStage(RequestStageType::kRequestFailed,
failed_status);
}
ExpectCommonUkm(GetParam().request_type,
/*category_id=*/80,
/*num_redirects=*/0,
/*num_retries=*/0,
/*is_context_detached=*/false,
RequestStageType::kRequestFailed,
RequestStageType::kRequestStarted, *request.keepalive_token,
failed_status.error_code, failed_status.extended_error_code);
ExpectTimeSortedTimeDeltaUkm({"TimeDelta.RequestStarted",
"TimeDelta.RequestFailed",
"TimeDelta.EventLogged"});
}
TEST_P(ChromeKeepAliveRequestTrackerTest, LoaderCompleted) {
auto request = CreateRequest(GetUrlWithCategory("test-prefix90"));
auto status =
CreateCompletionStatus(/*error_code=*/net::OK, /*extended_error_code=*/0);
{
auto tracker = CreateTracker(request);
ASSERT_THAT(tracker, NotNull());
FastForwardBy(one_time_step());
tracker->AdvanceToNextStage(RequestStageType::kRequestStarted);
FastForwardBy(one_time_step());
tracker->AdvanceToNextStage(RequestStageType::kResponseReceived);
FastForwardBy(one_time_step());
tracker->AdvanceToNextStage(RequestStageType::kLoaderCompleted, status);
}
ExpectCommonUkm(GetParam().request_type,
/*category_id=*/90,
/*num_redirects=*/0,
/*num_retries=*/0,
/*is_context_detached=*/false,
RequestStageType::kLoaderCompleted,
RequestStageType::kResponseReceived, *request.keepalive_token,
/*failed_error_code=*/std::nullopt,
/*failed_extended_error_code=*/std::nullopt,
status.error_code, status.extended_error_code);
ExpectTimeSortedTimeDeltaUkm(
{"TimeDelta.RequestStarted", "TimeDelta.ResponseReceived",
"TimeDelta.LoaderCompleted", "TimeDelta.EventLogged"});
}
TEST_P(ChromeKeepAliveRequestTrackerTest, LoaderCompletedWithError) {
auto request = CreateRequest(GetUrlWithCategory("test-prefix100"));
auto failed_status =
CreateCompletionStatus(/*error_code=*/15, /*extended_error_code=*/5);
{
auto tracker = CreateTracker(request);
ASSERT_THAT(tracker, NotNull());
FastForwardBy(one_time_step());
tracker->AdvanceToNextStage(RequestStageType::kRequestStarted);
FastForwardBy(one_time_step());
tracker->AdvanceToNextStage(RequestStageType::kLoaderCompleted,
failed_status);
}
ExpectCommonUkm(GetParam().request_type,
/*category_id=*/100,
/*num_redirects=*/0,
/*num_retries=*/0,
/*is_context_detached=*/false,
RequestStageType::kLoaderCompleted,
RequestStageType::kRequestStarted, *request.keepalive_token,
/*failed_error_code=*/std::nullopt,
/*failed_extended_error_code=*/std::nullopt,
failed_status.error_code, failed_status.extended_error_code);
ExpectTimeSortedTimeDeltaUkm(
{"TimeDelta.RequestStarted", "TimeDelta.LoaderCompleted",
"TimeDelta.EventLogged"});
}
TEST_P(ChromeKeepAliveRequestTrackerTest, RequestRetriedAfterRequestFailed) {
auto request = CreateRequest(GetUrlWithCategory("test-prefix110"));
auto failed_status = CreateCompletionStatus(/*error_code=*/net::ERR_TIMED_OUT,
/*extended_error_code=*/1);
auto retried_status =
CreateCompletionStatus(/*error_code=*/net::ERR_TIMED_OUT,
/*extended_error_code=*/1);
{
auto tracker = CreateTracker(request);
ASSERT_THAT(tracker, NotNull());
FastForwardBy(one_time_step());
tracker->AdvanceToNextStage(RequestStageType::kRequestStarted);
FastForwardBy(one_time_step());
tracker->AdvanceToNextStage(RequestStageType::kRequestFailed,
failed_status);
FastForwardBy(one_time_step());
tracker->AdvanceToNextStage(RequestStageType::kRequestRetried,
retried_status);
}
ExpectCommonUkm(
GetParam().request_type,
/*category_id=*/110,
/*num_redirects=*/0,
/*num_retries=*/1,
/*is_context_detached=*/false, RequestStageType::kRequestRetried,
RequestStageType::kRequestFailed, *request.keepalive_token,
/*failed_error_code=*/failed_status.error_code,
/*failed_extended_error_code=*/failed_status.extended_error_code,
/*completed_error_code=*/std::nullopt,
/*completed_extended_error_code=*/std::nullopt,
/*retried_error_code=*/retried_status.error_code,
/*retried_extended_error_code=*/retried_status.extended_error_code);
ExpectTimeSortedTimeDeltaUkm(
{"TimeDelta.RequestStarted", "TimeDelta.RequestFailed",
"TimeDelta.RequestRetried", "TimeDelta.EventLogged"});
}
TEST_P(ChromeKeepAliveRequestTrackerTest, RequestRetriedAfterTwoRedirects) {
auto request = CreateRequest(GetUrlWithCategory("test-prefix120"));
auto failed_status = CreateCompletionStatus(/*error_code=*/net::ERR_FAILED,
/*extended_error_code=*/2);
auto retried_status = CreateCompletionStatus(/*error_code=*/net::ERR_FAILED,
/*extended_error_code=*/2);
{
auto tracker = CreateTracker(request);
ASSERT_THAT(tracker, NotNull());
FastForwardBy(one_time_step());
tracker->AdvanceToNextStage(RequestStageType::kRequestStarted);
FastForwardBy(one_time_step());
tracker->AdvanceToNextStage(RequestStageType::kFirstRedirectReceived);
FastForwardBy(one_time_step());
tracker->AdvanceToNextStage(RequestStageType::kSecondRedirectReceived);
FastForwardBy(one_time_step());
tracker->AdvanceToNextStage(RequestStageType::kRequestFailed,
failed_status);
FastForwardBy(one_time_step());
tracker->AdvanceToNextStage(RequestStageType::kRequestRetried,
retried_status);
}
ExpectCommonUkm(
GetParam().request_type,
/*category_id=*/120,
/*num_redirects=*/2,
/*num_retries=*/1,
/*is_context_detached=*/false, RequestStageType::kRequestRetried,
RequestStageType::kRequestFailed, *request.keepalive_token,
/*failed_error_code=*/failed_status.error_code,
/*failed_extended_error_code=*/failed_status.extended_error_code,
/*completed_error_code=*/std::nullopt,
/*completed_extended_error_code=*/std::nullopt,
/*retried_error_code=*/retried_status.error_code,
/*retried_extended_error_code=*/retried_status.extended_error_code);
ExpectTimeSortedTimeDeltaUkm(
{"TimeDelta.RequestStarted", "TimeDelta.FirstRedirectReceived",
"TimeDelta.SecondRedirectReceived", "TimeDelta.RequestFailed",
"TimeDelta.RequestRetried", "TimeDelta.EventLogged"});
}
} // namespace