// Copyright 2014 The Chromium Authors. All rights reserved. // 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/bind.h" #include "base/callback.h" #include "base/command_line.h" #include "base/metrics/histogram_macros.h" #include "base/task/task_traits.h" #include "base/task/thread_pool.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 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 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 data, bool has_email) { reports_queue_.emplace(base::MakeRefCounted( feedback_reports_path_, base::Time::Now(), std::move(data), task_runner_, has_email)); UpdateUploadTimer(); } void FeedbackUploader::RequeueReport(scoped_refptr 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& a, const scoped_refptr& 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 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 } 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(); 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. 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 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 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 report = reports_queue_.top(); 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