Reporting / NEL: Allow limited NEL reports on Reporting uploads.

Change-Id: If7751d0316ac54e8c2e961c5b4e0385f1c6107a6
Reviewed-on: https://chromium-review.googlesource.com/978301
Commit-Queue: Julia Tuttle <[email protected]>
Reviewed-by: Asanka Herath <[email protected]>
Reviewed-by: Bernhard Bauer <[email protected]>
Cr-Commit-Position: refs/heads/master@{#546894}
diff --git a/chrome/browser/browsing_data/chrome_browsing_data_remover_delegate_unittest.cc b/chrome/browser/browsing_data/chrome_browsing_data_remover_delegate_unittest.cc
index f27f7a8..8d73eaa3 100644
--- a/chrome/browser/browsing_data/chrome_browsing_data_remover_delegate_unittest.cc
+++ b/chrome/browser/browsing_data/chrome_browsing_data_remover_delegate_unittest.cc
@@ -977,7 +977,8 @@
   void QueueReport(const GURL& url,
                    const std::string& group,
                    const std::string& type,
-                   std::unique_ptr<const base::Value> body) override {
+                   std::unique_ptr<const base::Value> body,
+                   int depth) override {
     NOTREACHED();
   }
 
@@ -994,9 +995,9 @@
     last_origin_filter_ = origin_filter;
   }
 
-  bool RequestIsUpload(const net::URLRequest& request) override {
+  int GetUploadDepth(const net::URLRequest& request) override {
     NOTREACHED();
-    return true;
+    return 0;
   }
 
   const net::ReportingPolicy& GetPolicy() const override {
diff --git a/content/browser/net/reporting_service_proxy.cc b/content/browser/net/reporting_service_proxy.cc
index 99f7d73..97fc3ec3 100644
--- a/content/browser/net/reporting_service_proxy.cc
+++ b/content/browser/net/reporting_service_proxy.cc
@@ -116,7 +116,10 @@
       return;
     }
 
-    reporting_service->QueueReport(url, group, type, std::move(body));
+    // Depth is only non-zero for NEL reports, and those can't come from the
+    // renderer.
+    reporting_service->QueueReport(url, group, type, std::move(body),
+                                   /* depth= */ 0);
   }
 
   scoped_refptr<net::URLRequestContextGetter> request_context_getter_;
diff --git a/net/network_error_logging/network_error_logging_service.cc b/net/network_error_logging/network_error_logging_service.cc
index b838000..6fdbcce 100644
--- a/net/network_error_logging/network_error_logging_service.cc
+++ b/net/network_error_logging/network_error_logging_service.cc
@@ -118,11 +118,6 @@
   return request.status_code >= 400 && request.status_code < 600;
 }
 
-bool RequestWasSuccessful(
-    const NetworkErrorLoggingService::RequestDetails& request) {
-  return request.type == OK && !IsHttpError(request);
-}
-
 enum class HeaderOutcome {
   DISCARDED_NO_NETWORK_ERROR_LOGGING_SERVICE = 0,
   DISCARDED_INVALID_SSL_INFO = 1,
@@ -234,28 +229,35 @@
       return;
     }
 
-    std::string type_string;
-    if (!GetTypeFromNetError(details.type, &type_string)) {
-      RecordRequestOutcome(RequestOutcome::DISCARDED_UNMAPPED_ERROR);
-      return;
-    }
-
+    Error type = details.type;
     // It is expected for Reporting uploads to terminate with ERR_ABORTED, since
     // the ReportingUploader cancels them after receiving the response code and
     // headers.
-    //
-    // (This check would go earlier, but the histogram bucket will be more
-    // meaningful if it only includes reports that otherwise could have been
-    // uploaded.)
-    if (details.is_reporting_upload && details.type == ERR_ABORTED) {
-      RecordRequestOutcome(RequestOutcome::DISCARDED_REPORTING_UPLOAD);
+    if (details.reporting_upload_depth > 0 && type == ERR_ABORTED) {
+      // TODO(juliatuttle): Modify ReportingUploader to drain successful uploads
+      // instead of aborting them, so NEL can properly report on aborted
+      // requests.
+      type = OK;
+    }
+
+    std::string type_string;
+    if (!GetTypeFromNetError(type, &type_string)) {
+      RecordRequestOutcome(RequestOutcome::DISCARDED_UNMAPPED_ERROR);
       return;
     }
 
     if (IsHttpError(details))
       type_string = kHttpErrorType;
 
-    bool success = RequestWasSuccessful(details);
+    // This check would go earlier, but the histogram bucket will be more
+    // meaningful if it only includes reports that otherwise could have been
+    // uploaded.
+    if (details.reporting_upload_depth > kMaxNestedReportDepth) {
+      RecordRequestOutcome(RequestOutcome::DISCARDED_REPORTING_UPLOAD);
+      return;
+    }
+
+    bool success = (type == OK) && !IsHttpError(details);
     double sampling_fraction =
         success ? policy->success_fraction : policy->failure_fraction;
     if (base::RandDouble() >= sampling_fraction) {
@@ -267,7 +269,8 @@
 
     reporting_service_->QueueReport(
         details.uri, policy->report_to, kReportType,
-        CreateReportBody(type_string, sampling_fraction, details));
+        CreateReportBody(type_string, sampling_fraction, details),
+        details.reporting_upload_depth);
     RecordRequestOutcome(RequestOutcome::QUEUED);
   }
 
@@ -500,6 +503,15 @@
 const char NetworkErrorLoggingService::kHeaderName[] = "NEL";
 
 const char NetworkErrorLoggingService::kReportType[] = "network-error";
+
+// Allow NEL reports on regular requests, plus NEL reports on Reporting uploads
+// containing only regular requests, but do not allow NEL reports on Reporting
+// uploads containing Reporting uploads.
+//
+// This prevents origins from building purposefully-broken Reporting endpoints
+// that generate new NEL reports to bypass the age limit on Reporting reports.
+const int NetworkErrorLoggingService::kMaxNestedReportDepth = 1;
+
 const char NetworkErrorLoggingService::kUriKey[] = "uri";
 const char NetworkErrorLoggingService::kReferrerKey[] = "referrer";
 const char NetworkErrorLoggingService::kSamplingFractionKey[] =
diff --git a/net/network_error_logging/network_error_logging_service.h b/net/network_error_logging/network_error_logging_service.h
index c41ae83..e63ece2 100644
--- a/net/network_error_logging/network_error_logging_service.h
+++ b/net/network_error_logging/network_error_logging_service.h
@@ -56,13 +56,22 @@
     base::TimeDelta elapsed_time;
     Error type;
 
-    bool is_reporting_upload;
+    // Upload nesting depth of this request.
+    //
+    // If the request is not a Reporting upload, the depth is 0.
+    //
+    // If the request is a Reporting upload, the depth is the max of the depth
+    // of the requests reported within it plus 1. (Non-NEL reports are
+    // considered to have depth 0.)
+    int reporting_upload_depth;
   };
 
   static const char kHeaderName[];
 
   static const char kReportType[];
 
+  static const int kMaxNestedReportDepth;
+
   // Keys for data included in report bodies. Exposed for tests.
 
   static const char kUriKey[];
diff --git a/net/network_error_logging/network_error_logging_service_unittest.cc b/net/network_error_logging/network_error_logging_service_unittest.cc
index 0cc3ab42..79d9681 100644
--- a/net/network_error_logging/network_error_logging_service_unittest.cc
+++ b/net/network_error_logging/network_error_logging_service_unittest.cc
@@ -35,13 +35,19 @@
         : url(other.url),
           group(other.group),
           type(other.type),
-          body(std::move(other.body)) {}
+          body(std::move(other.body)),
+          depth(other.depth) {}
 
     Report(const GURL& url,
            const std::string& group,
            const std::string& type,
-           std::unique_ptr<const base::Value> body)
-        : url(url), group(group), type(type), body(std::move(body)) {}
+           std::unique_ptr<const base::Value> body,
+           int depth)
+        : url(url),
+          group(group),
+          type(type),
+          body(std::move(body)),
+          depth(depth) {}
 
     ~Report() = default;
 
@@ -49,6 +55,7 @@
     std::string group;
     std::string type;
     std::unique_ptr<const base::Value> body;
+    int depth;
 
    private:
     DISALLOW_COPY(Report);
@@ -65,8 +72,9 @@
   void QueueReport(const GURL& url,
                    const std::string& group,
                    const std::string& type,
-                   std::unique_ptr<const base::Value> body) override {
-    reports_.push_back(Report(url, group, type, std::move(body)));
+                   std::unique_ptr<const base::Value> body,
+                   int depth) override {
+    reports_.push_back(Report(url, group, type, std::move(body), depth));
   }
 
   void ProcessHeader(const GURL& url,
@@ -80,9 +88,9 @@
     NOTREACHED();
   }
 
-  bool RequestIsUpload(const URLRequest& request) override {
+  int GetUploadDepth(const URLRequest& request) override {
     NOTREACHED();
-    return true;
+    return 0;
   }
 
   const ReportingPolicy& GetPolicy() const override {
@@ -130,7 +138,7 @@
     details.status_code = status_code;
     details.elapsed_time = base::TimeDelta::FromSeconds(1);
     details.type = error_type;
-    details.is_reporting_upload = false;
+    details.reporting_upload_depth = 0;
 
     return details;
   }
@@ -240,6 +248,7 @@
   EXPECT_EQ(kUrl_, reports()[0].url);
   EXPECT_EQ(kGroup_, reports()[0].group);
   EXPECT_EQ(kType_, reports()[0].type);
+  EXPECT_EQ(0, reports()[0].depth);
 
   const base::DictionaryValue* body;
   ASSERT_TRUE(reports()[0].body->GetAsDictionary(&body));
@@ -273,6 +282,7 @@
   EXPECT_EQ(kUrl_, reports()[0].url);
   EXPECT_EQ(kGroup_, reports()[0].group);
   EXPECT_EQ(kType_, reports()[0].type);
+  EXPECT_EQ(0, reports()[0].depth);
 
   const base::DictionaryValue* body;
   ASSERT_TRUE(reports()[0].body->GetAsDictionary(&body));
@@ -306,6 +316,7 @@
   EXPECT_EQ(kUrl_, reports()[0].url);
   EXPECT_EQ(kGroup_, reports()[0].group);
   EXPECT_EQ(kType_, reports()[0].type);
+  EXPECT_EQ(0, reports()[0].depth);
 
   const base::DictionaryValue* body;
   ASSERT_TRUE(reports()[0].body->GetAsDictionary(&body));
@@ -508,5 +519,31 @@
   ASSERT_EQ(1u, reports().size());
 }
 
+TEST_F(NetworkErrorLoggingServiceTest, Nested) {
+  service()->OnHeader(kOrigin_, kHeader_);
+
+  NetworkErrorLoggingService::RequestDetails details =
+      MakeRequestDetails(kUrl_, ERR_CONNECTION_REFUSED);
+  details.reporting_upload_depth =
+      NetworkErrorLoggingService::kMaxNestedReportDepth;
+  service()->OnRequest(details);
+
+  ASSERT_EQ(1u, reports().size());
+  EXPECT_EQ(NetworkErrorLoggingService::kMaxNestedReportDepth,
+            reports()[0].depth);
+}
+
+TEST_F(NetworkErrorLoggingServiceTest, NestedTooDeep) {
+  service()->OnHeader(kOrigin_, kHeader_);
+
+  NetworkErrorLoggingService::RequestDetails details =
+      MakeRequestDetails(kUrl_, ERR_CONNECTION_REFUSED);
+  details.reporting_upload_depth =
+      NetworkErrorLoggingService::kMaxNestedReportDepth + 1;
+  service()->OnRequest(details);
+
+  EXPECT_TRUE(reports().empty());
+}
+
 }  // namespace
 }  // namespace net
diff --git a/net/reporting/reporting_browsing_data_remover_unittest.cc b/net/reporting/reporting_browsing_data_remover_unittest.cc
index 3d8d10d81..7e720e98 100644
--- a/net/reporting/reporting_browsing_data_remover_unittest.cc
+++ b/net/reporting/reporting_browsing_data_remover_unittest.cc
@@ -41,7 +41,7 @@
 
   void AddReport(const GURL& url) {
     cache()->AddReport(url, kGroup_, kType_,
-                       std::make_unique<base::DictionaryValue>(),
+                       std::make_unique<base::DictionaryValue>(), 0,
                        tick_clock()->NowTicks(), 0);
   }
 
diff --git a/net/reporting/reporting_cache.cc b/net/reporting/reporting_cache.cc
index 14a61c2..4d4a1d0 100644
--- a/net/reporting/reporting_cache.cc
+++ b/net/reporting/reporting_cache.cc
@@ -67,10 +67,11 @@
                  const std::string& group,
                  const std::string& type,
                  std::unique_ptr<const base::Value> body,
+                 int depth,
                  base::TimeTicks queued,
                  int attempts) override {
     auto report = std::make_unique<ReportingReport>(
-        url, group, type, std::move(body), queued, attempts);
+        url, group, type, std::move(body), depth, queued, attempts);
 
     auto inserted =
         reports_.insert(std::make_pair(report.get(), std::move(report)));
diff --git a/net/reporting/reporting_cache.h b/net/reporting/reporting_cache.h
index e948748..a395f7f 100644
--- a/net/reporting/reporting_cache.h
+++ b/net/reporting/reporting_cache.h
@@ -47,6 +47,7 @@
                          const std::string& group,
                          const std::string& type,
                          std::unique_ptr<const base::Value> body,
+                         int depth,
                          base::TimeTicks queued,
                          int attempts) = 0;
 
diff --git a/net/reporting/reporting_cache_unittest.cc b/net/reporting/reporting_cache_unittest.cc
index 6e447fcf..de65ee4 100644
--- a/net/reporting/reporting_cache_unittest.cc
+++ b/net/reporting/reporting_cache_unittest.cc
@@ -94,7 +94,7 @@
   EXPECT_TRUE(reports.empty());
 
   cache()->AddReport(kUrl1_, kGroup1_, kType_,
-                     std::make_unique<base::DictionaryValue>(), kNow_, 0);
+                     std::make_unique<base::DictionaryValue>(), 0, kNow_, 0);
   EXPECT_EQ(1, observer()->cache_update_count());
 
   cache()->GetReports(&reports);
@@ -128,9 +128,9 @@
 
 TEST_F(ReportingCacheTest, RemoveAllReports) {
   cache()->AddReport(kUrl1_, kGroup1_, kType_,
-                     std::make_unique<base::DictionaryValue>(), kNow_, 0);
+                     std::make_unique<base::DictionaryValue>(), 0, kNow_, 0);
   cache()->AddReport(kUrl1_, kGroup1_, kType_,
-                     std::make_unique<base::DictionaryValue>(), kNow_, 0);
+                     std::make_unique<base::DictionaryValue>(), 0, kNow_, 0);
   EXPECT_EQ(2, observer()->cache_update_count());
 
   std::vector<const ReportingReport*> reports;
@@ -146,7 +146,7 @@
 
 TEST_F(ReportingCacheTest, RemovePendingReports) {
   cache()->AddReport(kUrl1_, kGroup1_, kType_,
-                     std::make_unique<base::DictionaryValue>(), kNow_, 0);
+                     std::make_unique<base::DictionaryValue>(), 0, kNow_, 0);
   EXPECT_EQ(1, observer()->cache_update_count());
 
   std::vector<const ReportingReport*> reports;
@@ -177,7 +177,7 @@
 
 TEST_F(ReportingCacheTest, RemoveAllPendingReports) {
   cache()->AddReport(kUrl1_, kGroup1_, kType_,
-                     std::make_unique<base::DictionaryValue>(), kNow_, 0);
+                     std::make_unique<base::DictionaryValue>(), 0, kNow_, 0);
   EXPECT_EQ(1, observer()->cache_update_count());
 
   std::vector<const ReportingReport*> reports;
@@ -410,7 +410,7 @@
   // Enqueue the maximum number of reports, spaced apart in time.
   for (size_t i = 0; i < max_report_count; ++i) {
     cache()->AddReport(kUrl1_, kGroup1_, kType_,
-                       std::make_unique<base::DictionaryValue>(),
+                       std::make_unique<base::DictionaryValue>(), 0,
                        tick_clock()->NowTicks(), 0);
     tick_clock()->Advance(base::TimeDelta::FromMinutes(1));
   }
@@ -418,7 +418,7 @@
 
   // Add one more report to force the cache to evict one.
   cache()->AddReport(kUrl1_, kGroup1_, kType_,
-                     std::make_unique<base::DictionaryValue>(), kNow_, 0);
+                     std::make_unique<base::DictionaryValue>(), 0, kNow_, 0);
 
   // Make sure the cache evicted a report to make room for the new one, and make
   // sure the report evicted was the earliest-queued one.
@@ -438,7 +438,7 @@
   // Enqueue the maximum number of reports, spaced apart in time.
   for (size_t i = 0; i < max_report_count; ++i) {
     cache()->AddReport(kUrl1_, kGroup1_, kType_,
-                       std::make_unique<base::DictionaryValue>(),
+                       std::make_unique<base::DictionaryValue>(), 0,
                        tick_clock()->NowTicks(), 0);
     tick_clock()->Advance(base::TimeDelta::FromMinutes(1));
   }
@@ -452,7 +452,7 @@
   // Add one more report to force the cache to evict one. Since the cache has
   // only pending reports, it will be forced to evict the *new* report!
   cache()->AddReport(kUrl1_, kGroup1_, kType_,
-                     std::make_unique<base::DictionaryValue>(), kNow_, 0);
+                     std::make_unique<base::DictionaryValue>(), 0, kNow_, 0);
 
   // Make sure the cache evicted a report, and make sure the report evicted was
   // the new, non-pending one.
diff --git a/net/reporting/reporting_delivery_agent.cc b/net/reporting/reporting_delivery_agent.cc
index 7a3ab2e..7419430 100644
--- a/net/reporting/reporting_delivery_agent.cc
+++ b/net/reporting/reporting_delivery_agent.cc
@@ -180,11 +180,16 @@
       std::string json;
       SerializeReports(reports, tick_clock()->NowTicks(), &json);
 
-      for (const ReportingReport* report : reports)
+      int max_depth = 0;
+      for (const ReportingReport* report : reports) {
         undelivered_reports.erase(report);
+        if (report->depth > max_depth)
+          max_depth = report->depth;
+      }
 
+      // TODO: Calculate actual max depth.
       uploader()->StartUpload(
-          endpoint, json,
+          endpoint, json, max_depth,
           base::BindOnce(
               &ReportingDeliveryAgentImpl::OnUploadComplete,
               weak_factory_.GetWeakPtr(),
diff --git a/net/reporting/reporting_delivery_agent_unittest.cc b/net/reporting/reporting_delivery_agent_unittest.cc
index c5b1a418..afd1f89 100644
--- a/net/reporting/reporting_delivery_agent_unittest.cc
+++ b/net/reporting/reporting_delivery_agent_unittest.cc
@@ -64,7 +64,7 @@
   body.SetString("key", "value");
 
   SetClient(kOrigin_, kEndpoint_, kGroup_);
-  cache()->AddReport(kUrl_, kGroup_, kType_, body.CreateDeepCopy(),
+  cache()->AddReport(kUrl_, kGroup_, kType_, body.CreateDeepCopy(), 0,
                      tick_clock()->NowTicks(), 0);
 
   tick_clock()->Advance(base::TimeDelta::FromMilliseconds(kAgeMillis));
@@ -103,7 +103,7 @@
 TEST_F(ReportingDeliveryAgentTest, FailedUpload) {
   SetClient(kOrigin_, kEndpoint_, kGroup_);
   cache()->AddReport(kUrl_, kGroup_, kType_,
-                     std::make_unique<base::DictionaryValue>(),
+                     std::make_unique<base::DictionaryValue>(), 0,
                      tick_clock()->NowTicks(), 0);
 
   EXPECT_TRUE(delivery_timer()->IsRunning());
@@ -137,7 +137,7 @@
   body.SetString("key", "value");
 
   SetClient(kOrigin_, kEndpoint_, kGroup_);
-  cache()->AddReport(kUrl_, kGroup_, kType_, body.CreateDeepCopy(),
+  cache()->AddReport(kUrl_, kGroup_, kType_, body.CreateDeepCopy(), 0,
                      tick_clock()->NowTicks(), 0);
 
   tick_clock()->Advance(base::TimeDelta::FromMilliseconds(kAgeMillis));
@@ -165,7 +165,7 @@
   ASSERT_TRUE(FindClientInCache(cache(), kDifferentOrigin, kEndpoint_));
 
   cache()->AddReport(kUrl_, kGroup_, kType_,
-                     std::make_unique<base::DictionaryValue>(),
+                     std::make_unique<base::DictionaryValue>(), 0,
                      tick_clock()->NowTicks(), 0);
 
   EXPECT_TRUE(delivery_timer()->IsRunning());
@@ -194,7 +194,7 @@
 TEST_F(ReportingDeliveryAgentTest, ConcurrentRemove) {
   SetClient(kOrigin_, kEndpoint_, kGroup_);
   cache()->AddReport(kUrl_, kGroup_, kType_,
-                     std::make_unique<base::DictionaryValue>(),
+                     std::make_unique<base::DictionaryValue>(), 0,
                      tick_clock()->NowTicks(), 0);
 
   EXPECT_TRUE(delivery_timer()->IsRunning());
@@ -233,7 +233,7 @@
 
   SetClient(kOrigin_, kEndpoint_, kGroup_);
   cache()->AddReport(kUrl_, kGroup_, kType_,
-                     std::make_unique<base::DictionaryValue>(),
+                     std::make_unique<base::DictionaryValue>(), 0,
                      tick_clock()->NowTicks(), 0);
 
   EXPECT_TRUE(delivery_timer()->IsRunning());
@@ -277,10 +277,10 @@
   SetClient(kDifferentOrigin, kEndpoint_, kGroup_);
 
   cache()->AddReport(kUrl_, kGroup_, kType_,
-                     std::make_unique<base::DictionaryValue>(),
+                     std::make_unique<base::DictionaryValue>(), 0,
                      tick_clock()->NowTicks(), 0);
   cache()->AddReport(kDifferentUrl, kGroup_, kType_,
-                     std::make_unique<base::DictionaryValue>(),
+                     std::make_unique<base::DictionaryValue>(), 0,
                      tick_clock()->NowTicks(), 0);
 
   EXPECT_TRUE(delivery_timer()->IsRunning());
@@ -303,7 +303,7 @@
   SetClient(kDifferentOrigin, kEndpoint_, kGroup_);
 
   cache()->AddReport(kUrl_, kGroup_, kType_,
-                     std::make_unique<base::DictionaryValue>(),
+                     std::make_unique<base::DictionaryValue>(), 0,
                      tick_clock()->NowTicks(), 0);
 
   EXPECT_TRUE(delivery_timer()->IsRunning());
@@ -311,7 +311,7 @@
   EXPECT_EQ(1u, pending_uploads().size());
 
   cache()->AddReport(kDifferentUrl, kGroup_, kType_,
-                     std::make_unique<base::DictionaryValue>(),
+                     std::make_unique<base::DictionaryValue>(), 0,
                      tick_clock()->NowTicks(), 0);
 
   EXPECT_TRUE(delivery_timer()->IsRunning());
@@ -339,7 +339,7 @@
   SetClient(kOrigin_, kDifferentEndpoint, kGroup_);
 
   cache()->AddReport(kUrl_, kGroup_, kType_,
-                     std::make_unique<base::DictionaryValue>(),
+                     std::make_unique<base::DictionaryValue>(), 0,
                      tick_clock()->NowTicks(), 0);
 
   EXPECT_TRUE(delivery_timer()->IsRunning());
@@ -347,7 +347,7 @@
   EXPECT_EQ(1u, pending_uploads().size());
 
   cache()->AddReport(kUrl_, kGroup_, kType_,
-                     std::make_unique<base::DictionaryValue>(),
+                     std::make_unique<base::DictionaryValue>(), 0,
                      tick_clock()->NowTicks(), 0);
 
   EXPECT_TRUE(delivery_timer()->IsRunning());
@@ -375,10 +375,10 @@
   SetClient(kOrigin_, kDifferentEndpoint, kDifferentGroup);
 
   cache()->AddReport(kUrl_, kGroup_, kType_,
-                     std::make_unique<base::DictionaryValue>(),
+                     std::make_unique<base::DictionaryValue>(), 0,
                      tick_clock()->NowTicks(), 0);
   cache()->AddReport(kUrl_, kDifferentGroup, kType_,
-                     std::make_unique<base::DictionaryValue>(),
+                     std::make_unique<base::DictionaryValue>(), 0,
                      tick_clock()->NowTicks(), 0);
 
   EXPECT_TRUE(delivery_timer()->IsRunning());
diff --git a/net/reporting/reporting_garbage_collector_unittest.cc b/net/reporting/reporting_garbage_collector_unittest.cc
index 3aac7abf..5ae177b 100644
--- a/net/reporting/reporting_garbage_collector_unittest.cc
+++ b/net/reporting/reporting_garbage_collector_unittest.cc
@@ -41,7 +41,7 @@
   EXPECT_FALSE(garbage_collection_timer()->IsRunning());
 
   cache()->AddReport(kUrl_, kGroup_, kType_,
-                     std::make_unique<base::DictionaryValue>(),
+                     std::make_unique<base::DictionaryValue>(), 0,
                      tick_clock()->NowTicks(), 0);
 
   EXPECT_TRUE(garbage_collection_timer()->IsRunning());
@@ -53,7 +53,7 @@
 
 TEST_F(ReportingGarbageCollectorTest, Report) {
   cache()->AddReport(kUrl_, kGroup_, kType_,
-                     std::make_unique<base::DictionaryValue>(),
+                     std::make_unique<base::DictionaryValue>(), 0,
                      tick_clock()->NowTicks(), 0);
   garbage_collection_timer()->Fire();
 
@@ -62,7 +62,7 @@
 
 TEST_F(ReportingGarbageCollectorTest, ExpiredReport) {
   cache()->AddReport(kUrl_, kGroup_, kType_,
-                     std::make_unique<base::DictionaryValue>(),
+                     std::make_unique<base::DictionaryValue>(), 0,
                      tick_clock()->NowTicks(), 0);
   tick_clock()->Advance(2 * policy().max_report_age);
   garbage_collection_timer()->Fire();
@@ -72,7 +72,7 @@
 
 TEST_F(ReportingGarbageCollectorTest, FailedReport) {
   cache()->AddReport(kUrl_, kGroup_, kType_,
-                     std::make_unique<base::DictionaryValue>(),
+                     std::make_unique<base::DictionaryValue>(), 0,
                      tick_clock()->NowTicks(), 0);
 
   std::vector<const ReportingReport*> reports;
diff --git a/net/reporting/reporting_network_change_observer_unittest.cc b/net/reporting/reporting_network_change_observer_unittest.cc
index ae35fbda8..db1aabc 100644
--- a/net/reporting/reporting_network_change_observer_unittest.cc
+++ b/net/reporting/reporting_network_change_observer_unittest.cc
@@ -65,7 +65,7 @@
   UsePolicy(new_policy);
 
   cache()->AddReport(kUrl_, kGroup_, kType_,
-                     std::make_unique<base::DictionaryValue>(),
+                     std::make_unique<base::DictionaryValue>(), 0,
                      tick_clock()->NowTicks(), 0);
   SetClient();
   ASSERT_EQ(1u, report_count());
@@ -84,7 +84,7 @@
   UsePolicy(new_policy);
 
   cache()->AddReport(kUrl_, kGroup_, kType_,
-                     std::make_unique<base::DictionaryValue>(),
+                     std::make_unique<base::DictionaryValue>(), 0,
                      tick_clock()->NowTicks(), 0);
   SetClient();
   ASSERT_EQ(1u, report_count());
@@ -103,7 +103,7 @@
   UsePolicy(new_policy);
 
   cache()->AddReport(kUrl_, kGroup_, kType_,
-                     std::make_unique<base::DictionaryValue>(),
+                     std::make_unique<base::DictionaryValue>(), 0,
                      tick_clock()->NowTicks(), 0);
   SetClient();
   ASSERT_EQ(1u, report_count());
@@ -122,7 +122,7 @@
   UsePolicy(new_policy);
 
   cache()->AddReport(kUrl_, kGroup_, kType_,
-                     std::make_unique<base::DictionaryValue>(),
+                     std::make_unique<base::DictionaryValue>(), 0,
                      tick_clock()->NowTicks(), 0);
   SetClient();
   ASSERT_EQ(1u, report_count());
diff --git a/net/reporting/reporting_report.cc b/net/reporting/reporting_report.cc
index 2a594841..c6dd8204 100644
--- a/net/reporting/reporting_report.cc
+++ b/net/reporting/reporting_report.cc
@@ -28,12 +28,14 @@
                                  const std::string& group,
                                  const std::string& type,
                                  std::unique_ptr<const base::Value> body,
+                                 int depth,
                                  base::TimeTicks queued,
                                  int attempts)
     : url(url),
       group(group),
       type(type),
       body(std::move(body)),
+      depth(depth),
       queued(queued),
       attempts(attempts),
       outcome(Outcome::UNKNOWN),
diff --git a/net/reporting/reporting_report.h b/net/reporting/reporting_report.h
index e7fba5d..196be9e 100644
--- a/net/reporting/reporting_report.h
+++ b/net/reporting/reporting_report.h
@@ -41,6 +41,7 @@
                   const std::string& group,
                   const std::string& type,
                   std::unique_ptr<const base::Value> body,
+                  int depth,
                   base::TimeTicks queued,
                   int attempts);
   ~ReportingReport();
@@ -64,6 +65,11 @@
   // The body of the report. (Included in the delivered report.)
   std::unique_ptr<const base::Value> body;
 
+  // How many uploads deep the related request was: 0 if the related request was
+  // not an upload (or there was no related request), or n+1 if it was an upload
+  // reporting on requests of at most depth n.
+  int depth;
+
   // When the report was queued. (Included in the delivered report as an age
   // relative to the time of the delivery attempt.)
   base::TimeTicks queued;
diff --git a/net/reporting/reporting_service.cc b/net/reporting/reporting_service.cc
index 24be577..ce1efb0 100644
--- a/net/reporting/reporting_service.cc
+++ b/net/reporting/reporting_service.cc
@@ -36,14 +36,15 @@
   void QueueReport(const GURL& url,
                    const std::string& group,
                    const std::string& type,
-                   std::unique_ptr<const base::Value> body) override {
+                   std::unique_ptr<const base::Value> body,
+                   int depth) override {
     DCHECK(context_);
     DCHECK(context_->delegate());
 
     if (!context_->delegate()->CanQueueReport(url::Origin::Create(url)))
       return;
 
-    context_->cache()->AddReport(url, group, type, std::move(body),
+    context_->cache()->AddReport(url, group, type, std::move(body), depth,
                                  context_->tick_clock()->NowTicks(), 0);
   }
 
@@ -64,8 +65,8 @@
         context_->cache(), data_type_mask, origin_filter);
   }
 
-  bool RequestIsUpload(const URLRequest& request) override {
-    return context_->uploader()->RequestIsUpload(request);
+  int GetUploadDepth(const URLRequest& request) override {
+    return context_->uploader()->GetUploadDepth(request);
   }
 
   const ReportingPolicy& GetPolicy() const override {
diff --git a/net/reporting/reporting_service.h b/net/reporting/reporting_service.h
index 37958702..b165cdcd 100644
--- a/net/reporting/reporting_service.h
+++ b/net/reporting/reporting_service.h
@@ -53,7 +53,8 @@
   virtual void QueueReport(const GURL& url,
                            const std::string& group,
                            const std::string& type,
-                           std::unique_ptr<const base::Value> body) = 0;
+                           std::unique_ptr<const base::Value> body,
+                           int depth) = 0;
 
   // Processes a Report-To header. |url| is the URL that originated the header;
   // |header_value| is the normalized value of the Report-To header.
@@ -66,9 +67,9 @@
       int data_type_mask,
       const base::RepeatingCallback<bool(const GURL&)>& origin_filter) = 0;
 
-  // Checks whether |request| is a Reporting upload, to avoid loops of reporting
-  // about report uploads.
-  virtual bool RequestIsUpload(const URLRequest& request) = 0;
+  // Checks how many uploads deep |request| is: 0 if it's not an upload, n+1 if
+  // it's an upload reporting on requests of at most depth n.
+  virtual int GetUploadDepth(const URLRequest& request) = 0;
 
   virtual const ReportingPolicy& GetPolicy() const = 0;
 
diff --git a/net/reporting/reporting_service_unittest.cc b/net/reporting/reporting_service_unittest.cc
index 6b9f303..c0dd7fe5 100644
--- a/net/reporting/reporting_service_unittest.cc
+++ b/net/reporting/reporting_service_unittest.cc
@@ -48,7 +48,7 @@
 
 TEST_F(ReportingServiceTest, QueueReport) {
   service()->QueueReport(kUrl_, kGroup_, kType_,
-                         std::make_unique<base::DictionaryValue>());
+                         std::make_unique<base::DictionaryValue>(), 0);
 
   std::vector<const ReportingReport*> reports;
   context()->cache()->GetReports(&reports);
diff --git a/net/reporting/reporting_test_util.cc b/net/reporting/reporting_test_util.cc
index e998b58..e87f889 100644
--- a/net/reporting/reporting_test_util.cc
+++ b/net/reporting/reporting_test_util.cc
@@ -46,7 +46,7 @@
 
   ~PendingUploadImpl() override = default;
 
-  // PendingUpload implementationP:
+  // PendingUpload implementation:
   const GURL& url() const override { return url_; }
   const std::string& json() const override { return json_; }
   std::unique_ptr<base::Value> GetValue() const override {
@@ -100,15 +100,16 @@
 
 void TestReportingUploader::StartUpload(const GURL& url,
                                         const std::string& json,
+                                        int max_depth,
                                         UploadCallback callback) {
   pending_uploads_.push_back(std::make_unique<PendingUploadImpl>(
       url, json, std::move(callback),
       base::BindOnce(&ErasePendingUpload, &pending_uploads_)));
 }
 
-bool TestReportingUploader::RequestIsUpload(const URLRequest& request) {
+int TestReportingUploader::GetUploadDepth(const URLRequest& request) {
   NOTIMPLEMENTED();
-  return true;
+  return 0;
 }
 
 TestReportingDelegate::TestReportingDelegate()
diff --git a/net/reporting/reporting_test_util.h b/net/reporting/reporting_test_util.h
index ff387ecf4..296827b 100644
--- a/net/reporting/reporting_test_util.h
+++ b/net/reporting/reporting_test_util.h
@@ -73,9 +73,10 @@
 
   void StartUpload(const GURL& url,
                    const std::string& json,
+                   int max_depth,
                    UploadCallback callback) override;
 
-  bool RequestIsUpload(const URLRequest& request) override;
+  int GetUploadDepth(const URLRequest& request) override;
 
  private:
   std::vector<std::unique_ptr<PendingUpload>> pending_uploads_;
diff --git a/net/reporting/reporting_uploader.cc b/net/reporting/reporting_uploader.cc
index 2353726..9cd1273 100644
--- a/net/reporting/reporting_uploader.cc
+++ b/net/reporting/reporting_uploader.cc
@@ -27,6 +27,10 @@
 class UploadUserData : public base::SupportsUserData::Data {
  public:
   static const void* const kUserDataKey;
+
+  UploadUserData(int depth) : depth(depth) {}
+
+  int depth;
 };
 
 // SetUserData needs a unique const void* to serve as the key, so create a const
@@ -77,6 +81,7 @@
 
   void StartUpload(const GURL& url,
                    const std::string& json,
+                   int max_depth,
                    UploadCallback callback) override {
     net::NetworkTrafficAnnotationTag traffic_annotation =
         net::DefineNetworkTrafficAnnotation("reporting", R"(
@@ -116,7 +121,7 @@
         ElementsUploadDataStream::CreateWithReader(std::move(reader), 0));
 
     request->SetUserData(UploadUserData::kUserDataKey,
-                         std::make_unique<UploadUserData>());
+                         std::make_unique<UploadUserData>(max_depth));
 
     // This inherently sets mode = "no-cors", but that doesn't matter, because
     // the origins that are included in the upload don't actually get to see
@@ -132,8 +137,10 @@
   }
 
   // static
-  bool RequestIsUpload(const net::URLRequest& request) override {
-    return request.GetUserData(UploadUserData::kUserDataKey);
+  int GetUploadDepth(const net::URLRequest& request) override {
+    UploadUserData* data = static_cast<UploadUserData*>(
+        request.GetUserData(UploadUserData::kUserDataKey));
+    return data ? data->depth + 1 : 0;
   }
 
   // URLRequest::Delegate implementation:
diff --git a/net/reporting/reporting_uploader.h b/net/reporting/reporting_uploader.h
index f6c65c7..b17c0c49 100644
--- a/net/reporting/reporting_uploader.h
+++ b/net/reporting/reporting_uploader.h
@@ -34,10 +34,11 @@
   // |url|, and calls |callback| when complete (whether successful or not).
   virtual void StartUpload(const GURL& url,
                            const std::string& json,
+                           int max_depth,
                            UploadCallback callback) = 0;
 
   // Returns whether |request| is an upload request sent by this uploader.
-  virtual bool RequestIsUpload(const URLRequest& request) = 0;
+  virtual int GetUploadDepth(const URLRequest& request) = 0;
 
   // Creates a real implementation of |ReportingUploader| that uploads reports
   // using |context|.
diff --git a/net/reporting/reporting_uploader_unittest.cc b/net/reporting/reporting_uploader_unittest.cc
index 9fcda4d..e417b08d 100644
--- a/net/reporting/reporting_uploader_unittest.cc
+++ b/net/reporting/reporting_uploader_unittest.cc
@@ -109,7 +109,8 @@
   ASSERT_TRUE(server_.Start());
 
   TestUploadCallback callback;
-  uploader_->StartUpload(server_.GetURL("/"), kUploadBody, callback.callback());
+  uploader_->StartUpload(server_.GetURL("/"), kUploadBody, 0,
+                         callback.callback());
   callback.WaitForCall();
 }
 
@@ -118,7 +119,8 @@
   ASSERT_TRUE(server_.Start());
 
   TestUploadCallback callback;
-  uploader_->StartUpload(server_.GetURL("/"), kUploadBody, callback.callback());
+  uploader_->StartUpload(server_.GetURL("/"), kUploadBody, 0,
+                         callback.callback());
   callback.WaitForCall();
 
   EXPECT_EQ(ReportingUploader::Outcome::SUCCESS, callback.outcome());
@@ -130,7 +132,7 @@
   ASSERT_TRUE(server_.ShutdownAndWaitUntilComplete());
 
   TestUploadCallback callback;
-  uploader_->StartUpload(url, kUploadBody, callback.callback());
+  uploader_->StartUpload(url, kUploadBody, 0, callback.callback());
   callback.WaitForCall();
 
   EXPECT_EQ(ReportingUploader::Outcome::FAILURE, callback.outcome());
@@ -141,7 +143,8 @@
   ASSERT_TRUE(server_.Start());
 
   TestUploadCallback callback;
-  uploader_->StartUpload(server_.GetURL("/"), kUploadBody, callback.callback());
+  uploader_->StartUpload(server_.GetURL("/"), kUploadBody, 0,
+                         callback.callback());
   callback.WaitForCall();
 
   EXPECT_EQ(ReportingUploader::Outcome::FAILURE, callback.outcome());
@@ -153,7 +156,8 @@
   ASSERT_TRUE(server_.Start());
 
   TestUploadCallback callback;
-  uploader_->StartUpload(server_.GetURL("/"), kUploadBody, callback.callback());
+  uploader_->StartUpload(server_.GetURL("/"), kUploadBody, 0,
+                         callback.callback());
   callback.WaitForCall();
 
   EXPECT_EQ(ReportingUploader::Outcome::FAILURE, callback.outcome());
@@ -165,7 +169,8 @@
   ASSERT_TRUE(server_.Start());
 
   TestUploadCallback callback;
-  uploader_->StartUpload(server_.GetURL("/"), kUploadBody, callback.callback());
+  uploader_->StartUpload(server_.GetURL("/"), kUploadBody, 0,
+                         callback.callback());
   callback.WaitForCall();
 
   EXPECT_EQ(ReportingUploader::Outcome::REMOVE_ENDPOINT, callback.outcome());
@@ -207,7 +212,8 @@
   ASSERT_TRUE(server_.Start());
 
   TestUploadCallback callback;
-  uploader_->StartUpload(server_.GetURL("/"), kUploadBody, callback.callback());
+  uploader_->StartUpload(server_.GetURL("/"), kUploadBody, 0,
+                         callback.callback());
   callback.WaitForCall();
 
   EXPECT_TRUE(followed);
@@ -228,7 +234,8 @@
   ASSERT_TRUE(server_.Start());
 
   TestUploadCallback callback;
-  uploader_->StartUpload(server_.GetURL("/"), kUploadBody, callback.callback());
+  uploader_->StartUpload(server_.GetURL("/"), kUploadBody, 0,
+                         callback.callback());
   callback.WaitForCall();
 
   EXPECT_FALSE(followed);
@@ -254,7 +261,7 @@
   ASSERT_TRUE(cookie_callback.result());
 
   TestUploadCallback upload_callback;
-  uploader_->StartUpload(server_.GetURL("/"), kUploadBody,
+  uploader_->StartUpload(server_.GetURL("/"), kUploadBody, 0,
                          upload_callback.callback());
   upload_callback.WaitForCall();
 }
@@ -274,7 +281,7 @@
   ASSERT_TRUE(server_.Start());
 
   TestUploadCallback upload_callback;
-  uploader_->StartUpload(server_.GetURL("/"), kUploadBody,
+  uploader_->StartUpload(server_.GetURL("/"), kUploadBody, 0,
                          upload_callback.callback());
   upload_callback.WaitForCall();
 
@@ -312,7 +319,7 @@
 
   {
     TestUploadCallback callback;
-    uploader_->StartUpload(server_.GetURL("/"), kUploadBody,
+    uploader_->StartUpload(server_.GetURL("/"), kUploadBody, 0,
                            callback.callback());
     callback.WaitForCall();
   }
@@ -320,7 +327,7 @@
 
   {
     TestUploadCallback callback;
-    uploader_->StartUpload(server_.GetURL("/"), kUploadBody,
+    uploader_->StartUpload(server_.GetURL("/"), kUploadBody, 0,
                            callback.callback());
     callback.WaitForCall();
   }
diff --git a/net/url_request/url_request.cc b/net/url_request/url_request.cc
index 8e18063..cd187ce 100644
--- a/net/url_request/url_request.cc
+++ b/net/url_request/url_request.cc
@@ -1196,9 +1196,12 @@
       base::TimeTicks::Now() - load_timing_info_.request_start;
   details.type = status().ToNetError();
 
-  details.is_reporting_upload =
-      context()->reporting_service() &&
-      context()->reporting_service()->RequestIsUpload(*this);
+  if (context()->reporting_service()) {
+    details.reporting_upload_depth =
+        context()->reporting_service()->GetUploadDepth(*this);
+  } else {
+    details.reporting_upload_depth = 0;
+  }
 
   service->OnRequest(details);
 }
diff --git a/net/url_request/url_request_unittest.cc b/net/url_request/url_request_unittest.cc
index 023a29af..dc599e8 100644
--- a/net/url_request/url_request_unittest.cc
+++ b/net/url_request/url_request_unittest.cc
@@ -7116,7 +7116,8 @@
   void QueueReport(const GURL& url,
                    const std::string& group,
                    const std::string& type,
-                   std::unique_ptr<const base::Value> body) override {
+                   std::unique_ptr<const base::Value> body,
+                   int depth) override {
     NOTIMPLEMENTED();
   }
 
@@ -7131,9 +7132,9 @@
     NOTIMPLEMENTED();
   }
 
-  bool RequestIsUpload(const URLRequest& request) override {
+  int GetUploadDepth(const URLRequest& request) override {
     NOTIMPLEMENTED();
-    return true;
+    return 0;
   }
 
   const ReportingPolicy& GetPolicy() const override {