blob: cedada33d4442df2683efafd65a2b17575bf0130 [file] [log] [blame]
// Copyright 2014 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "components/feedback/feedback_uploader.h"
#include "base/command_line.h"
#include "base/functional/bind.h"
#include "base/functional/callback.h"
#include "base/metrics/histogram_macros.h"
#include "base/task/single_thread_task_runner.h"
#include "base/task/task_traits.h"
#include "base/task/thread_pool.h"
#include "components/feedback/features.h"
#include "components/feedback/feedback_report.h"
#include "components/feedback/feedback_switches.h"
#include "components/variations/net/variations_http_headers.h"
#include "net/base/load_flags.h"
#include "services/network/public/cpp/resource_request.h"
#include "services/network/public/cpp/simple_url_loader.h"
#include "services/network/public/mojom/url_response_head.mojom.h"
namespace feedback {
namespace {
constexpr char kReportSendingResultHistogramName[] =
"Feedback.ReportSending.Result";
// These values are persisted to logs. Entries should not be renumbered and
// numeric values should never be reused.
enum class FeedbackReportSendingResult {
kSuccessAtFirstTry = 0, // The report was uploaded successfully without retry
kSuccessAfterRetry = 1, // The report was uploaded successfully after retry
kDropped = 2, // The report is corrupt or invalid and was dropped
kMaxValue = kDropped,
};
constexpr base::FilePath::CharType kFeedbackReportPath[] =
FILE_PATH_LITERAL("Feedback Reports");
constexpr char kFeedbackPostUrl[] =
"https://www.google.com/tools/feedback/chrome/__submit";
constexpr char kProtoBufMimeType[] = "application/x-protobuf";
constexpr int kHttpPostSuccessNoContent = 204;
constexpr int kHttpPostFailNoConnection = -1;
constexpr int kHttpPostFailClientError = 400;
constexpr int kHttpPostFailServerError = 500;
// The minimum time to wait before uploading reports are retried. Exponential
// backoff delay is applied on successive failures.
// This value can be overriden by tests by calling
// FeedbackUploader::SetMinimumRetryDelayForTesting().
base::TimeDelta g_minimum_retry_delay = base::Minutes(60);
// If a new report is queued to be dispatched immediately while another is being
// dispatched, this is the time to wait for the on-going dispatching to finish.
base::TimeDelta g_dispatching_wait_delay = base::Seconds(4);
GURL GetFeedbackPostGURL() {
const base::CommandLine& command_line =
*base::CommandLine::ForCurrentProcess();
return GURL(command_line.HasSwitch(switches::kFeedbackServer)
? command_line.GetSwitchValueASCII(switches::kFeedbackServer)
: kFeedbackPostUrl);
}
// Creates a new SingleThreadTaskRunner that is used to run feedback blocking
// background work.
scoped_refptr<base::SingleThreadTaskRunner> CreateUploaderTaskRunner() {
// Uses a BLOCK_SHUTDOWN file task runner to prevent losing reports or
// corrupting report's files.
return base::ThreadPool::CreateSingleThreadTaskRunner(
{base::MayBlock(), base::TaskPriority::BEST_EFFORT,
base::TaskShutdownBehavior::BLOCK_SHUTDOWN});
}
} // namespace
FeedbackUploader::FeedbackUploader(
bool is_off_the_record,
const base::FilePath& state_path,
SharedURLLoaderFactoryGetter shared_url_loader_factory_getter)
: FeedbackUploader(is_off_the_record,
state_path,
std::move(shared_url_loader_factory_getter),
nullptr) {}
FeedbackUploader::FeedbackUploader(
bool is_off_the_record,
const base::FilePath& state_path,
scoped_refptr<network::SharedURLLoaderFactory> shared_url_loader_factory)
: FeedbackUploader(is_off_the_record,
state_path,
SharedURLLoaderFactoryGetter(),
shared_url_loader_factory) {}
FeedbackUploader::~FeedbackUploader() {}
// static
void FeedbackUploader::SetMinimumRetryDelayForTesting(base::TimeDelta delay) {
g_minimum_retry_delay = delay;
}
void FeedbackUploader::QueueReport(std::unique_ptr<std::string> data,
bool has_email,
int product_id) {
reports_queue_.emplace(base::MakeRefCounted<FeedbackReport>(
feedback_reports_path_, base::Time::Now(), std::move(data), task_runner_,
has_email, product_id));
UpdateUploadTimer();
}
void FeedbackUploader::RequeueReport(scoped_refptr<FeedbackReport> report) {
DCHECK_EQ(task_runner_, report->reports_task_runner());
report->set_upload_at(base::Time::Now());
reports_queue_.emplace(std::move(report));
UpdateUploadTimer();
}
void FeedbackUploader::StartDispatchingReport() {
DispatchReport();
}
void FeedbackUploader::OnReportUploadSuccess() {
if (retry_delay_ == g_minimum_retry_delay) {
UMA_HISTOGRAM_ENUMERATION(kReportSendingResultHistogramName,
FeedbackReportSendingResult::kSuccessAtFirstTry);
} else {
UMA_HISTOGRAM_ENUMERATION(kReportSendingResultHistogramName,
FeedbackReportSendingResult::kSuccessAfterRetry);
}
retry_delay_ = g_minimum_retry_delay;
is_dispatching_ = false;
// Explicitly release the successfully dispatched report.
report_being_dispatched_->DeleteReportOnDisk();
report_being_dispatched_ = nullptr;
UpdateUploadTimer();
}
void FeedbackUploader::OnReportUploadFailure(bool should_retry) {
if (should_retry) {
// Implement a backoff delay by doubling the retry delay on each failure.
retry_delay_ *= 2;
report_being_dispatched_->set_upload_at(retry_delay_ + base::Time::Now());
reports_queue_.emplace(report_being_dispatched_);
} else {
// The report won't be retried, hence explicitly delete its file on disk.
report_being_dispatched_->DeleteReportOnDisk();
UMA_HISTOGRAM_ENUMERATION(kReportSendingResultHistogramName,
FeedbackReportSendingResult::kDropped);
}
// The report dispatching failed, and should either be retried or not. In all
// cases, we need to release |report_being_dispatched_|. If it was up for
// retry, then it has already been re-enqueued and will be kept alive.
// Otherwise we're done with it and it should destruct.
report_being_dispatched_ = nullptr;
is_dispatching_ = false;
UpdateUploadTimer();
}
bool FeedbackUploader::ReportsUploadTimeComparator::operator()(
const scoped_refptr<FeedbackReport>& a,
const scoped_refptr<FeedbackReport>& b) const {
return a->upload_at() > b->upload_at();
}
FeedbackUploader::FeedbackUploader(
bool is_off_the_record,
const base::FilePath& state_path,
SharedURLLoaderFactoryGetter url_loader_factory_getter,
scoped_refptr<network::SharedURLLoaderFactory> url_loader_factory)
: url_loader_factory_getter_(std::move(url_loader_factory_getter)),
url_loader_factory_(url_loader_factory),
feedback_reports_path_(state_path.Append(kFeedbackReportPath)),
task_runner_(CreateUploaderTaskRunner()),
feedback_post_url_(GetFeedbackPostGURL()),
retry_delay_(g_minimum_retry_delay),
is_off_the_record_(is_off_the_record) {
DCHECK(!!url_loader_factory_getter_ != !!url_loader_factory_);
}
void FeedbackUploader::AppendExtraHeadersToUploadRequest(
network::ResourceRequest* resource_request) {}
void FeedbackUploader::DispatchReport() {
net::NetworkTrafficAnnotationTag traffic_annotation =
net::DefineNetworkTrafficAnnotation("chrome_feedback_report_app", R"(
semantics {
sender: "Chrome Feedback Report App"
description:
"Users can press Alt+Shift+i to report a bug or a feedback in "
"general. Along with the free-form text they entered, system logs "
"that helps in diagnosis of the issue are sent to Google. This "
"service uploads the report to Google Feedback server."
trigger:
"When user chooses to send a feedback to Google."
data:
"The free-form text that user has entered and useful debugging "
"logs (UI logs, Chrome logs, kernel logs, auto update engine logs, "
"ARC++ logs, etc.). The logs are redacted to remove any "
"user-private data. The user can view the system information "
"before sending, and choose to send the feedback report without "
"system information and the logs (unchecking 'Send system "
"information' prevents sending logs as well), the screenshot, or "
"even his/her email address."
destination: GOOGLE_OWNED_SERVICE
internal {
contacts {
email: "cros-feedback-app@google.com"
}
}
user_data {
type: ARBITRARY_DATA
type: EMAIL
type: IMAGE
type: USER_CONTENT
}
last_reviewed: "2023-08-14"
}
policy {
cookies_allowed: NO
setting:
"This feature cannot be disabled by settings and is only activated "
"by direct user request."
chrome_policy {
UserFeedbackAllowed {
UserFeedbackAllowed: false
}
}
})");
auto resource_request = std::make_unique<network::ResourceRequest>();
resource_request->url = feedback_post_url_;
resource_request->credentials_mode = network::mojom::CredentialsMode::kOmit;
resource_request->method = "POST";
// Tell feedback server about the variation state of this install.
if (report_being_dispatched_->should_include_variations()) {
variations::AppendVariationsHeaderUnknownSignedIn(
feedback_post_url_,
is_off_the_record_ ? variations::InIncognito::kYes
: variations::InIncognito::kNo,
resource_request.get());
}
if (report_being_dispatched_->has_email()) {
AppendExtraHeadersToUploadRequest(resource_request.get());
}
std::unique_ptr<network::SimpleURLLoader> simple_url_loader =
network::SimpleURLLoader::Create(std::move(resource_request),
traffic_annotation);
network::SimpleURLLoader* simple_url_loader_ptr = simple_url_loader.get();
simple_url_loader->AttachStringForUpload(report_being_dispatched_->data(),
kProtoBufMimeType);
auto it = uploads_in_progress_.insert(uploads_in_progress_.begin(),
std::move(simple_url_loader));
if (!url_loader_factory_) {
// Lazily create the URLLoaderFactory.
url_loader_factory_ = std::move(url_loader_factory_getter_).Run();
DCHECK(url_loader_factory_);
}
simple_url_loader_ptr->DownloadToStringOfUnboundedSizeUntilCrashAndDie(
url_loader_factory_.get(),
base::BindOnce(&FeedbackUploader::OnDispatchComplete,
base::Unretained(this), std::move(it)));
}
void FeedbackUploader::OnDispatchComplete(
UrlLoaderList::iterator it,
std::unique_ptr<std::string> response_body) {
std::stringstream error_stream;
network::SimpleURLLoader* simple_url_loader = it->get();
int response_code = kHttpPostFailNoConnection;
if (simple_url_loader->ResponseInfo() &&
simple_url_loader->ResponseInfo()->headers) {
response_code = simple_url_loader->ResponseInfo()->headers->response_code();
}
if (response_code == kHttpPostSuccessNoContent) {
error_stream << "Success";
OnReportUploadSuccess();
} else {
bool should_retry = true;
// Process the error for debug output
if (response_code == kHttpPostFailNoConnection) {
error_stream << "No connection to server.";
} else if ((response_code >= kHttpPostFailClientError) &&
(response_code < kHttpPostFailServerError)) {
// Client errors mean that the server failed to parse the proto that was
// sent, or that some requirements weren't met by the server side
// validation, and hence we should NOT retry sending this corrupt report
// and give up.
should_retry = false;
error_stream << "Client error: HTTP response code " << response_code;
} else if (response_code >= kHttpPostFailServerError) {
error_stream << "Server error: HTTP response code " << response_code;
} else {
error_stream << "Unknown error: HTTP response code " << response_code;
}
OnReportUploadFailure(should_retry);
}
LOG(WARNING) << "FEEDBACK: Submission to feedback server ("
<< simple_url_loader->GetFinalURL()
<< ") status: " << error_stream.str();
uploads_in_progress_.erase(it);
}
void FeedbackUploader::UpdateUploadTimer() {
if (reports_queue_.empty())
return;
scoped_refptr<FeedbackReport> report = reports_queue_.top();
// Don't send reports in Tast tests so that they don't spam Listnr.
if (feedback::features::IsSkipSendingFeedbackReportInTastTestsEnabled()) {
report->DeleteReportOnDisk();
reports_queue_.pop();
return;
}
const base::Time now = base::Time::Now();
if (report->upload_at() <= now && !is_dispatching_) {
reports_queue_.pop();
is_dispatching_ = true;
report_being_dispatched_ = report;
StartDispatchingReport();
} else {
// Stop the old timer and start an updated one.
const base::TimeDelta delay = (is_dispatching_ || now > report->upload_at())
? g_dispatching_wait_delay
: report->upload_at() - now;
upload_timer_.Stop();
upload_timer_.Start(FROM_HERE, delay, this,
&FeedbackUploader::UpdateUploadTimer);
}
}
} // namespace feedback