Add report-only mode to Feature Policy

This change adds a "report-only" policy to each security context, which
is local to that context (not replicated across processes) and is used
to determine whether a report should be sent even if a feature is
enabled, when the feature is used.

Reports are now augmented with a "disposition" field, which is either
"enforce", if the feature usage was actually blocked, or "report", if
it was not.

Feature policy directives are placed in the report-only policy if the
feature name is suffixed with "-report-only", otherwise, they affect
the regular (enforcing) policy.

Explainer at
https://github.com/WICG/feature-policy/blob/master/reporting.md

Existing tests are updated, and new tests for report-only mode are
added to ensure that reports are sent even when the feature is used
successfully.

Bug: 904878
Change-Id: I27bc42729c5ab5560160f3d993431e606a8a3a47
Reviewed-on: https://chromium-review.googlesource.com/c/1178811
Commit-Queue: Ian Clelland <iclelland@chromium.org>
Reviewed-by: Ken Buchanan <kenrb@chromium.org>
Reviewed-by: Dmitry Gozman <dgozman@chromium.org>
Cr-Commit-Position: refs/heads/master@{#608004}
diff --git a/content/browser/frame_host/render_frame_host_feature_policy_unittest.cc b/content/browser/frame_host/render_frame_host_feature_policy_unittest.cc
index b730cee3..5510718 100644
--- a/content/browser/frame_host/render_frame_host_feature_policy_unittest.cc
+++ b/content/browser/frame_host/render_frame_host_feature_policy_unittest.cc
@@ -84,6 +84,7 @@
     blink::ParsedFeaturePolicy result(1);
     result[0].feature = feature;
     result[0].matches_all_origins = false;
+    result[0].disposition = blink::mojom::FeaturePolicyDisposition::kEnforce;
     for (const std::string& origin : origins)
       result[0].origins.push_back(url::Origin::Create(GURL(origin)));
     return result;
diff --git a/content/browser/frame_host/render_frame_host_impl.cc b/content/browser/frame_host/render_frame_host_impl.cc
index d73589c..a44d629 100644
--- a/content/browser/frame_host/render_frame_host_impl.cc
+++ b/content/browser/frame_host/render_frame_host_impl.cc
@@ -2820,7 +2820,8 @@
     return;
   // Rebuild the feature policy for this frame.
   ResetFeaturePolicy();
-  feature_policy_->SetHeaderPolicy(parsed_header);
+  feature_policy_->SetHeaderPolicy(*DirectivesWithDisposition(
+      blink::mojom::FeaturePolicyDisposition::kEnforce, parsed_header));
 
   // Update the feature policy and sandbox flags in the frame tree. This will
   // send any updates to proxies if necessary.
@@ -5437,7 +5438,10 @@
   blink::ParsedFeaturePolicy container_policy =
       frame_tree_node()->effective_frame_policy().container_policy;
   feature_policy_ = blink::FeaturePolicy::CreateFromParentPolicy(
-      parent_policy, container_policy, last_committed_origin_);
+      parent_policy,
+      *DirectivesWithDisposition(
+          blink::mojom::FeaturePolicyDisposition::kEnforce, container_policy),
+      last_committed_origin_);
 }
 
 void RenderFrameHostImpl::CreateAudioInputStreamFactory(
diff --git a/content/browser/net/reporting_service_proxy.cc b/content/browser/net/reporting_service_proxy.cc
index d207fa4..f661d32 100644
--- a/content/browser/net/reporting_service_proxy.cc
+++ b/content/browser/net/reporting_service_proxy.cc
@@ -108,12 +108,14 @@
   void QueueFeaturePolicyViolationReport(
       const GURL& url,
       const std::string& policy,
+      const std::string& disposition,
       const std::string& message,
       const base::Optional<std::string>& source_file,
       int line_number,
       int column_number) override {
     auto body = std::make_unique<base::DictionaryValue>();
     body->SetString("policy", policy);
+    body->SetString("disposition", disposition);
     body->SetString("message", message);
     if (source_file)
       body->SetString("sourceFile", *source_file);
diff --git a/content/browser/site_per_process_browsertest.cc b/content/browser/site_per_process_browsertest.cc
index eafffaa..92eae27 100644
--- a/content/browser/site_per_process_browsertest.cc
+++ b/content/browser/site_per_process_browsertest.cc
@@ -479,6 +479,7 @@
   blink::ParsedFeaturePolicy result(1);
   result[0].feature = feature;
   result[0].matches_all_origins = false;
+  result[0].disposition = blink::mojom::FeaturePolicyDisposition::kEnforce;
   DCHECK(!origins.empty());
   for (const GURL& origin : origins)
     result[0].origins.push_back(url::Origin::Create(origin));
@@ -492,6 +493,7 @@
   blink::ParsedFeaturePolicy result(1);
   result[0].feature = feature;
   result[0].matches_all_origins = true;
+  result[0].disposition = blink::mojom::FeaturePolicyDisposition::kEnforce;
   return result;
 }
 
diff --git a/content/common/frame_messages.h b/content/common/frame_messages.h
index b23fcca1..8984848f 100644
--- a/content/common/frame_messages.h
+++ b/content/common/frame_messages.h
@@ -153,6 +153,8 @@
 IPC_ENUM_TRAITS_MIN_MAX_VALUE(content::NavigationDownloadPolicy,
                               content::NavigationDownloadPolicy::kAllow,
                               content::NavigationDownloadPolicy::kMaxValue)
+IPC_ENUM_TRAITS_MAX_VALUE(blink::mojom::FeaturePolicyDisposition,
+                          blink::mojom::FeaturePolicyDisposition::kMaxValue)
 
 IPC_STRUCT_TRAITS_BEGIN(blink::WebFloatSize)
   IPC_STRUCT_TRAITS_MEMBER(width)
@@ -534,6 +536,7 @@
   IPC_STRUCT_TRAITS_MEMBER(feature)
   IPC_STRUCT_TRAITS_MEMBER(matches_all_origins)
   IPC_STRUCT_TRAITS_MEMBER(matches_opaque_src)
+  IPC_STRUCT_TRAITS_MEMBER(disposition)
   IPC_STRUCT_TRAITS_MEMBER(origins)
 IPC_STRUCT_TRAITS_END()
 
diff --git a/content/test/test_render_frame_host.cc b/content/test/test_render_frame_host.cc
index c5628b1..e75fbfd 100644
--- a/content/test/test_render_frame_host.cc
+++ b/content/test/test_render_frame_host.cc
@@ -272,6 +272,7 @@
   blink::ParsedFeaturePolicy header(1);
   header[0].feature = feature;
   header[0].matches_all_origins = false;
+  header[0].disposition = blink::mojom::FeaturePolicyDisposition::kEnforce;
   header[0].origins = whitelist;
   DidSetFramePolicyHeaders(blink::WebSandboxFlags::kNone, header);
 }
diff --git a/third_party/WebKit/LayoutTests/external/wpt/feature-policy/reporting/camera-report-only.https.html b/third_party/WebKit/LayoutTests/external/wpt/feature-policy/reporting/camera-report-only.https.html
new file mode 100644
index 0000000..2648868
--- /dev/null
+++ b/third_party/WebKit/LayoutTests/external/wpt/feature-policy/reporting/camera-report-only.https.html
@@ -0,0 +1,29 @@
+<!DOCTYPE html>
+<html>
+  <head>
+    <script src='/resources/testharness.js'></script>
+    <script src='/resources/testharnessreport.js'></script>
+    <script src='/resources/testdriver.js'></script>
+    <script src="/resources/testdriver-vendor.js"></script>
+  </head>
+  <body>
+    <script>
+var check_report_format = ([reports, observer]) => {
+  let report = reports[0];
+  assert_equals(report.type, "feature-policy");
+  assert_equals(report.body.feature, "camera");
+  assert_equals(report.body.disposition, "report");
+};
+
+promise_test(async t => {
+  const report = new Promise(resolve => {
+    new ReportingObserver((reports, observer) => resolve([reports, observer]),
+                          {types: ['feature-policy']}).observe();
+  });
+  await test_driver.bless('Activate document for user media');
+  await navigator.mediaDevices.getUserMedia({video: true});
+  check_report_format(await report);
+}, "Camera report only mode");
+    </script>
+  </body>
+</html>
diff --git a/third_party/WebKit/LayoutTests/external/wpt/feature-policy/reporting/camera-report-only.https.html.headers b/third_party/WebKit/LayoutTests/external/wpt/feature-policy/reporting/camera-report-only.https.html.headers
new file mode 100644
index 0000000..46b8481
--- /dev/null
+++ b/third_party/WebKit/LayoutTests/external/wpt/feature-policy/reporting/camera-report-only.https.html.headers
@@ -0,0 +1 @@
+Feature-Policy: camera-report-only 'none'
diff --git a/third_party/WebKit/LayoutTests/external/wpt/feature-policy/reporting/camera-reporting.https.html b/third_party/WebKit/LayoutTests/external/wpt/feature-policy/reporting/camera-reporting.https.html
index 14b2ed1d..1d08700d 100644
--- a/third_party/WebKit/LayoutTests/external/wpt/feature-policy/reporting/camera-reporting.https.html
+++ b/third_party/WebKit/LayoutTests/external/wpt/feature-policy/reporting/camera-reporting.https.html
@@ -13,6 +13,7 @@
   assert_equals(report.type, "feature-policy");
   assert_equals(report.url, document.location.href);
   assert_equals(report.body.feature, "camera");
+  assert_equals(report.body.disposition, "enforce");
   assert_equals(report.body.sourceFile, document.location.href);
   assert_equals(typeof report.body.message, "string");
   assert_equals(typeof report.body.lineNumber, "number");
diff --git a/third_party/WebKit/LayoutTests/external/wpt/feature-policy/reporting/document-write-report-only.html b/third_party/WebKit/LayoutTests/external/wpt/feature-policy/reporting/document-write-report-only.html
new file mode 100644
index 0000000..ab0bb827
--- /dev/null
+++ b/third_party/WebKit/LayoutTests/external/wpt/feature-policy/reporting/document-write-report-only.html
@@ -0,0 +1,26 @@
+<!DOCTYPE html>
+<html>
+  <head>
+    <script src='/resources/testharness.js'></script>
+    <script src='/resources/testharnessreport.js'></script>
+  </head>
+  <body>
+    <script>
+var check_report_format = ([reports, observer]) => {
+  let report = reports[0];
+  assert_equals(report.type, "feature-policy");
+  assert_equals(report.body.feature, "document-write");
+  assert_equals(report.body.disposition, "report");
+};
+
+promise_test(async t => {
+  const report = new Promise(resolve => {
+    new ReportingObserver((reports, observer) => resolve([reports, observer]),
+                          {types: ['feature-policy']}).observe();
+  });
+  document.write("This should be written into the document");
+  check_report_format(await report);
+}, "Document-write report only mode");
+    </script>
+  </body>
+</html>
diff --git a/third_party/WebKit/LayoutTests/external/wpt/feature-policy/reporting/document-write-report-only.html.headers b/third_party/WebKit/LayoutTests/external/wpt/feature-policy/reporting/document-write-report-only.html.headers
new file mode 100644
index 0000000..63e43f1d
--- /dev/null
+++ b/third_party/WebKit/LayoutTests/external/wpt/feature-policy/reporting/document-write-report-only.html.headers
@@ -0,0 +1 @@
+Feature-Policy: document-write-report-only 'none'
diff --git a/third_party/WebKit/LayoutTests/external/wpt/feature-policy/reporting/document-write-reporting.html b/third_party/WebKit/LayoutTests/external/wpt/feature-policy/reporting/document-write-reporting.html
index cb08b8d..d113f2e 100644
--- a/third_party/WebKit/LayoutTests/external/wpt/feature-policy/reporting/document-write-reporting.html
+++ b/third_party/WebKit/LayoutTests/external/wpt/feature-policy/reporting/document-write-reporting.html
@@ -13,6 +13,7 @@
   assert_equals(report.type, "feature-policy");
   assert_equals(report.url, document.location.href);
   assert_equals(report.body.feature, "document-write");
+  assert_equals(report.body.disposition, "enforce");
   assert_equals(report.body.sourceFile, document.location.href);
   assert_equals(typeof report.body.message, "string");
   assert_equals(typeof report.body.lineNumber, "number");
diff --git a/third_party/WebKit/LayoutTests/external/wpt/feature-policy/reporting/encrypted-media-report-only.https.html b/third_party/WebKit/LayoutTests/external/wpt/feature-policy/reporting/encrypted-media-report-only.https.html
new file mode 100644
index 0000000..20e44b2f
--- /dev/null
+++ b/third_party/WebKit/LayoutTests/external/wpt/feature-policy/reporting/encrypted-media-report-only.https.html
@@ -0,0 +1,30 @@
+<!DOCTYPE html>
+<html>
+  <head>
+    <script src='/resources/testharness.js'></script>
+    <script src='/resources/testharnessreport.js'></script>
+  </head>
+  <body>
+    <script>
+var check_report_format = ([reports, observer]) => {
+  let report = reports[0];
+  assert_equals(report.type, "feature-policy");
+  assert_equals(report.body.feature, "encrypted-media");
+  assert_equals(report.body.disposition, "report");
+};
+
+promise_test(async t => {
+  const report = new Promise(resolve => {
+    new ReportingObserver((reports, observer) => resolve([reports, observer]),
+                          {types: ['feature-policy']}).observe();
+  });
+  await navigator.requestMediaKeySystemAccess("org.w3.clearkey",
+      [{
+        initDataTypes: ["webm"],
+        videoCapabilities: [{contentType: 'video/webm;codecs="vp8"'}],
+      }]);
+  check_report_format(await report);
+}, "Encrypted Media report only mode");
+    </script>
+  </body>
+</html>
diff --git a/third_party/WebKit/LayoutTests/external/wpt/feature-policy/reporting/encrypted-media-report-only.https.html.headers b/third_party/WebKit/LayoutTests/external/wpt/feature-policy/reporting/encrypted-media-report-only.https.html.headers
new file mode 100644
index 0000000..a2c8fbb
--- /dev/null
+++ b/third_party/WebKit/LayoutTests/external/wpt/feature-policy/reporting/encrypted-media-report-only.https.html.headers
@@ -0,0 +1 @@
+Feature-Policy: encrypted-media-report-only 'none'
diff --git a/third_party/WebKit/LayoutTests/external/wpt/feature-policy/reporting/encrypted-media-reporting.https.html b/third_party/WebKit/LayoutTests/external/wpt/feature-policy/reporting/encrypted-media-reporting.https.html
index d309d53..c3b6393 100644
--- a/third_party/WebKit/LayoutTests/external/wpt/feature-policy/reporting/encrypted-media-reporting.https.html
+++ b/third_party/WebKit/LayoutTests/external/wpt/feature-policy/reporting/encrypted-media-reporting.https.html
@@ -11,6 +11,7 @@
   assert_equals(report.type, "feature-policy");
   assert_equals(report.url, document.location.href);
   assert_equals(report.body.feature, "encrypted-media");
+  assert_equals(report.body.disposition, "enforce");
   assert_equals(report.body.sourceFile, document.location.href);
   assert_equals(typeof report.body.message, "string");
   assert_equals(typeof report.body.lineNumber, "number");
diff --git a/third_party/WebKit/LayoutTests/external/wpt/feature-policy/reporting/fullscreen-report-only.html b/third_party/WebKit/LayoutTests/external/wpt/feature-policy/reporting/fullscreen-report-only.html
new file mode 100644
index 0000000..a6b3d5ad
--- /dev/null
+++ b/third_party/WebKit/LayoutTests/external/wpt/feature-policy/reporting/fullscreen-report-only.html
@@ -0,0 +1,31 @@
+<!DOCTYPE html>
+<html>
+  <head>
+    <script src='/resources/testharness.js'></script>
+    <script src='/resources/testharnessreport.js'></script>
+    <script src='/resources/testdriver.js'></script>
+    <script src="/resources/testdriver-vendor.js"></script>
+  </head>
+  <body>
+    <div id='fs'></div>
+    <script>
+var check_report_format = ([reports, observer]) => {
+  let report = reports[0];
+  assert_equals(report.type, "feature-policy");
+  assert_equals(report.body.feature, "fullscreen");
+  assert_equals(report.body.disposition, "report");
+};
+
+promise_test(async t => {
+  const report = new Promise(resolve => {
+    new ReportingObserver((reports, observer) => resolve([reports, observer]),
+                          {types: ['feature-policy']}).observe();
+  });
+  await test_driver.bless('Activate document for fullscreen');
+  await document.getElementById('fs').requestFullscreen();
+  check_report_format(await report);
+  document.exitFullscreen();
+}, "Fullscreen report only mode");
+    </script>
+  </body>
+</html>
diff --git a/third_party/WebKit/LayoutTests/external/wpt/feature-policy/reporting/fullscreen-report-only.html.headers b/third_party/WebKit/LayoutTests/external/wpt/feature-policy/reporting/fullscreen-report-only.html.headers
new file mode 100644
index 0000000..33defa88
--- /dev/null
+++ b/third_party/WebKit/LayoutTests/external/wpt/feature-policy/reporting/fullscreen-report-only.html.headers
@@ -0,0 +1 @@
+Feature-Policy: fullscreen-report-only 'none'
diff --git a/third_party/WebKit/LayoutTests/external/wpt/feature-policy/reporting/fullscreen-reporting.html b/third_party/WebKit/LayoutTests/external/wpt/feature-policy/reporting/fullscreen-reporting.html
index 83d97c9..9a7fb87 100644
--- a/third_party/WebKit/LayoutTests/external/wpt/feature-policy/reporting/fullscreen-reporting.html
+++ b/third_party/WebKit/LayoutTests/external/wpt/feature-policy/reporting/fullscreen-reporting.html
@@ -12,6 +12,7 @@
   assert_equals(report.type, "feature-policy");
   assert_equals(report.url, document.location.href);
   assert_equals(report.body.feature, "fullscreen");
+  assert_equals(report.body.disposition, "enforce");
   assert_equals(report.body.sourceFile, document.location.href);
   assert_equals(typeof report.body.message, "string");
   assert_equals(typeof report.body.lineNumber, "number");
diff --git a/third_party/WebKit/LayoutTests/external/wpt/feature-policy/reporting/generic-sensor-report-only.https.html b/third_party/WebKit/LayoutTests/external/wpt/feature-policy/reporting/generic-sensor-report-only.https.html
new file mode 100644
index 0000000..deb6ade
--- /dev/null
+++ b/third_party/WebKit/LayoutTests/external/wpt/feature-policy/reporting/generic-sensor-report-only.https.html
@@ -0,0 +1,56 @@
+<!DOCTYPE html>
+<html>
+  <head>
+    <script src='/resources/testharness.js'></script>
+    <script src='/resources/testharnessreport.js'></script>
+  </head>
+  <body>
+    <script>
+var sensor_features_verified = {
+  "accelerometer": false,
+  "ambient-light-sensor": false,
+  "magnetometer": false,
+  "gyroscope": false
+};
+
+var check_report_format = function(reports, observer) {
+  // Check each report in this batch. This observer callback may be called
+  // multiple times before all reports have been processed.
+  for (const report of reports) {
+
+    // Validate that the reported feature is one of the sensor features, and that
+    // we have not seen a report for this feature before.
+    assert_true(sensor_features_verified.hasOwnProperty(report.body.feature));
+    assert_false(sensor_features_verified[report.body.feature]);
+
+    // Validate the remainder of the report
+    assert_equals(report.type, "feature-policy");
+    assert_equals(report.url, document.location.href);
+    assert_equals(report.body.disposition, "report");
+    assert_equals(report.body.sourceFile, document.location.href);
+    assert_equals(typeof report.body.message, "string");
+    assert_equals(typeof report.body.lineNumber, "number");
+    assert_equals(typeof report.body.columnNumber, "number");
+
+    sensor_features_verified[report.body.feature] = true;
+  }
+
+  // Test is only done when reports for all features have been seen
+  for (let result of Object.values(sensor_features_verified)) {
+    if (!result)
+      return;
+  }
+  this.done();
+};
+
+async_test(t => {
+  new ReportingObserver(t.step_func(check_report_format),
+                        {types: ['feature-policy']}).observe();
+  new Accelerometer();
+  new AmbientLightSensor();
+  new Gyroscope();
+  new Magnetometer();
+}, "Generic Sensor report only mode");
+    </script>
+  </body>
+</html>
diff --git a/third_party/WebKit/LayoutTests/external/wpt/feature-policy/reporting/generic-sensor-report-only.https.html.headers b/third_party/WebKit/LayoutTests/external/wpt/feature-policy/reporting/generic-sensor-report-only.https.html.headers
new file mode 100644
index 0000000..26605eb0
--- /dev/null
+++ b/third_party/WebKit/LayoutTests/external/wpt/feature-policy/reporting/generic-sensor-report-only.https.html.headers
@@ -0,0 +1 @@
+Feature-Policy: ambient-light-sensor-report-only 'none'; accelerometer-report-only 'none'; gyroscope-report-only 'none'; magnetometer-report-only 'none'
diff --git a/third_party/WebKit/LayoutTests/external/wpt/feature-policy/reporting/generic-sensor-reporting.https.html b/third_party/WebKit/LayoutTests/external/wpt/feature-policy/reporting/generic-sensor-reporting.https.html
index c60e3e8..517c7f6 100644
--- a/third_party/WebKit/LayoutTests/external/wpt/feature-policy/reporting/generic-sensor-reporting.https.html
+++ b/third_party/WebKit/LayoutTests/external/wpt/feature-policy/reporting/generic-sensor-reporting.https.html
@@ -26,6 +26,7 @@
     // Validate the remainder of the report
     assert_equals(report.type, "feature-policy");
     assert_equals(report.url, document.location.href);
+    assert_equals(report.body.disposition, "enforce");
     assert_equals(report.body.sourceFile, document.location.href);
     assert_equals(typeof report.body.message, "string");
     assert_equals(typeof report.body.lineNumber, "number");
diff --git a/third_party/WebKit/LayoutTests/external/wpt/feature-policy/reporting/geolocation-report-only.https-expected.txt b/third_party/WebKit/LayoutTests/external/wpt/feature-policy/reporting/geolocation-report-only.https-expected.txt
new file mode 100644
index 0000000..9322061
--- /dev/null
+++ b/third_party/WebKit/LayoutTests/external/wpt/feature-policy/reporting/geolocation-report-only.https-expected.txt
@@ -0,0 +1,4 @@
+This is a testharness.js-based test.
+FAIL Geolocation report only mode promise_test: Unhandled rejection with value: object "[object PositionError]"
+Harness: the test ran to completion.
+
diff --git a/third_party/WebKit/LayoutTests/external/wpt/feature-policy/reporting/geolocation-report-only.https.html b/third_party/WebKit/LayoutTests/external/wpt/feature-policy/reporting/geolocation-report-only.https.html
new file mode 100644
index 0000000..cf2a75b
--- /dev/null
+++ b/third_party/WebKit/LayoutTests/external/wpt/feature-policy/reporting/geolocation-report-only.https.html
@@ -0,0 +1,36 @@
+<!DOCTYPE html>
+<html>
+  <head>
+    <script src='/resources/testharness.js'></script>
+    <script src='/resources/testharnessreport.js'></script>
+  </head>
+  <body>
+    <script>
+var check_report_format = ([reports, observer]) => {
+  let report = reports[0];
+  assert_equals(report.type, "feature-policy");
+  assert_equals(report.body.feature, "geolocation");
+  assert_equals(report.body.disposition, "report");
+};
+
+promise_test(async t => {
+  const report = new Promise(resolve => {
+    new ReportingObserver((reports, observer) => resolve([reports, observer]),
+                          {types: ['feature-policy']}).observe();
+  });
+  try {
+    await new Promise((resolve, reject) => {
+      navigator.geolocation.getCurrentPosition(resolve, reject);
+    });
+    check_report_format(await report);
+  } catch (err) {
+    // In case the getCurrentPosition call was rejected due to user permissions,
+    // the report should be generated anyway. Wait for it and check the format
+    // before failing this test.
+    check_report_format(await report);
+    throw err;
+  }
+}, "Geolocation report only mode");
+    </script>
+  </body>
+</html>
diff --git a/third_party/WebKit/LayoutTests/external/wpt/feature-policy/reporting/geolocation-report-only.https.html.headers b/third_party/WebKit/LayoutTests/external/wpt/feature-policy/reporting/geolocation-report-only.https.html.headers
new file mode 100644
index 0000000..fc985900
--- /dev/null
+++ b/third_party/WebKit/LayoutTests/external/wpt/feature-policy/reporting/geolocation-report-only.https.html.headers
@@ -0,0 +1 @@
+Feature-Policy: geolocation-report-only 'none'
diff --git a/third_party/WebKit/LayoutTests/external/wpt/feature-policy/reporting/geolocation-reporting.https.html b/third_party/WebKit/LayoutTests/external/wpt/feature-policy/reporting/geolocation-reporting.https.html
index 22e2585..ce06902 100644
--- a/third_party/WebKit/LayoutTests/external/wpt/feature-policy/reporting/geolocation-reporting.https.html
+++ b/third_party/WebKit/LayoutTests/external/wpt/feature-policy/reporting/geolocation-reporting.https.html
@@ -13,6 +13,7 @@
   assert_equals(report.type, "feature-policy");
   assert_equals(report.url, document.location.href);
   assert_equals(report.body.feature, "geolocation");
+  assert_equals(report.body.disposition, "enforce");
   assert_equals(report.body.sourceFile, document.location.href);
   assert_equals(typeof report.body.message, "string");
   assert_equals(typeof report.body.lineNumber, "number");
diff --git a/third_party/WebKit/LayoutTests/external/wpt/feature-policy/reporting/microphone-report-only.https.html b/third_party/WebKit/LayoutTests/external/wpt/feature-policy/reporting/microphone-report-only.https.html
new file mode 100644
index 0000000..2d7b4d96
--- /dev/null
+++ b/third_party/WebKit/LayoutTests/external/wpt/feature-policy/reporting/microphone-report-only.https.html
@@ -0,0 +1,29 @@
+<!DOCTYPE html>
+<html>
+  <head>
+    <script src='/resources/testharness.js'></script>
+    <script src='/resources/testharnessreport.js'></script>
+    <script src='/resources/testdriver.js'></script>
+    <script src="/resources/testdriver-vendor.js"></script>
+  </head>
+  <body>
+    <script>
+var check_report_format = ([reports, observer]) => {
+  let report = reports[0];
+  assert_equals(report.type, "feature-policy");
+  assert_equals(report.body.feature, "microphone");
+  assert_equals(report.body.disposition, "report");
+};
+
+promise_test(async t => {
+  const report = new Promise(resolve => {
+    new ReportingObserver((reports, observer) => resolve([reports, observer]),
+                          {types: ['feature-policy']}).observe();
+  });
+  await test_driver.bless('Activate document for user media');
+  await navigator.mediaDevices.getUserMedia({audio: true});
+  check_report_format(await report);
+}, "Microphone report only mode");
+    </script>
+  </body>
+</html>
diff --git a/third_party/WebKit/LayoutTests/external/wpt/feature-policy/reporting/microphone-report-only.https.html.headers b/third_party/WebKit/LayoutTests/external/wpt/feature-policy/reporting/microphone-report-only.https.html.headers
new file mode 100644
index 0000000..7673d05
--- /dev/null
+++ b/third_party/WebKit/LayoutTests/external/wpt/feature-policy/reporting/microphone-report-only.https.html.headers
@@ -0,0 +1 @@
+Feature-Policy: microphone-report-only 'none'
diff --git a/third_party/WebKit/LayoutTests/external/wpt/feature-policy/reporting/microphone-reporting.https.html b/third_party/WebKit/LayoutTests/external/wpt/feature-policy/reporting/microphone-reporting.https.html
index 7347a23..4a0e0b5 100644
--- a/third_party/WebKit/LayoutTests/external/wpt/feature-policy/reporting/microphone-reporting.https.html
+++ b/third_party/WebKit/LayoutTests/external/wpt/feature-policy/reporting/microphone-reporting.https.html
@@ -13,6 +13,7 @@
   assert_equals(report.type, "feature-policy");
   assert_equals(report.url, document.location.href);
   assert_equals(report.body.feature, "microphone");
+  assert_equals(report.body.disposition, "enforce");
   assert_equals(report.body.sourceFile, document.location.href);
   assert_equals(typeof report.body.message, "string");
   assert_equals(typeof report.body.lineNumber, "number");
diff --git a/third_party/WebKit/LayoutTests/external/wpt/feature-policy/reporting/midi-report-only-expected.txt b/third_party/WebKit/LayoutTests/external/wpt/feature-policy/reporting/midi-report-only-expected.txt
new file mode 100644
index 0000000..bba8e2a
--- /dev/null
+++ b/third_party/WebKit/LayoutTests/external/wpt/feature-policy/reporting/midi-report-only-expected.txt
@@ -0,0 +1,4 @@
+This is a testharness.js-based test.
+FAIL MIDI report only mode promise_test: Unhandled rejection with value: object "SecurityError: An attempt was made to break through the security policy of the user agent."
+Harness: the test ran to completion.
+
diff --git a/third_party/WebKit/LayoutTests/external/wpt/feature-policy/reporting/midi-report-only.html b/third_party/WebKit/LayoutTests/external/wpt/feature-policy/reporting/midi-report-only.html
new file mode 100644
index 0000000..e466ce0d
--- /dev/null
+++ b/third_party/WebKit/LayoutTests/external/wpt/feature-policy/reporting/midi-report-only.html
@@ -0,0 +1,34 @@
+<!DOCTYPE html>
+<html>
+  <head>
+    <script src='/resources/testharness.js'></script>
+    <script src='/resources/testharnessreport.js'></script>
+  </head>
+  <body>
+    <script>
+var check_report_format = ([reports, observer]) => {
+  let report = reports[0];
+  assert_equals(report.type, "feature-policy");
+  assert_equals(report.body.feature, "midi");
+  assert_equals(report.body.disposition, "report");
+};
+
+promise_test(async t => {
+  const report = new Promise(resolve => {
+    new ReportingObserver((reports, observer) => resolve([reports, observer]),
+                          {types: ['feature-policy']}).observe();
+  });
+  try {
+    await navigator.requestMIDIAccess();
+    check_report_format(await report);
+  } catch (err) {
+    // In case the requestMIDIAccess call was rejected due to user permissions,
+    // the report should be generated anyway. Wait for it and check the format
+    // before failing this test.
+    check_report_format(await report);
+    throw err;
+  }
+}, "MIDI report only mode");
+    </script>
+  </body>
+</html>
diff --git a/third_party/WebKit/LayoutTests/external/wpt/feature-policy/reporting/midi-report-only.html.headers b/third_party/WebKit/LayoutTests/external/wpt/feature-policy/reporting/midi-report-only.html.headers
new file mode 100644
index 0000000..3c6a2d4
--- /dev/null
+++ b/third_party/WebKit/LayoutTests/external/wpt/feature-policy/reporting/midi-report-only.html.headers
@@ -0,0 +1 @@
+Feature-Policy: midi-report-only 'none'
diff --git a/third_party/WebKit/LayoutTests/external/wpt/feature-policy/reporting/midi-reporting.html b/third_party/WebKit/LayoutTests/external/wpt/feature-policy/reporting/midi-reporting.html
index 8303b7a..fc6d8b1a 100644
--- a/third_party/WebKit/LayoutTests/external/wpt/feature-policy/reporting/midi-reporting.html
+++ b/third_party/WebKit/LayoutTests/external/wpt/feature-policy/reporting/midi-reporting.html
@@ -11,6 +11,7 @@
   assert_equals(report.type, "feature-policy");
   assert_equals(report.url, document.location.href);
   assert_equals(report.body.feature, "midi");
+  assert_equals(report.body.disposition, "enforce");
   assert_equals(report.body.sourceFile, document.location.href);
   assert_equals(typeof report.body.message, "string");
   assert_equals(typeof report.body.lineNumber, "number");
diff --git a/third_party/WebKit/LayoutTests/external/wpt/feature-policy/reporting/payment-report-only.https-expected.txt b/third_party/WebKit/LayoutTests/external/wpt/feature-policy/reporting/payment-report-only.https-expected.txt
new file mode 100644
index 0000000..19f3d28
--- /dev/null
+++ b/third_party/WebKit/LayoutTests/external/wpt/feature-policy/reporting/payment-report-only.https-expected.txt
@@ -0,0 +1,4 @@
+This is a testharness.js-based test.
+FAIL PaymentRequest report only mode promise_test: Unhandled rejection with value: object "UnknownError: Request failed"
+Harness: the test ran to completion.
+
diff --git a/third_party/WebKit/LayoutTests/external/wpt/feature-policy/reporting/payment-report-only.https.html b/third_party/WebKit/LayoutTests/external/wpt/feature-policy/reporting/payment-report-only.https.html
new file mode 100644
index 0000000..6a7678b5
--- /dev/null
+++ b/third_party/WebKit/LayoutTests/external/wpt/feature-policy/reporting/payment-report-only.https.html
@@ -0,0 +1,37 @@
+<!DOCTYPE html>
+<html>
+  <head>
+    <script src='/resources/testharness.js'></script>
+    <script src='/resources/testharnessreport.js'></script>
+  </head>
+  <body>
+    <script>
+var check_report_format = ([reports, observer]) => {
+  let report = reports[0];
+  assert_equals(report.type, "feature-policy");
+  assert_equals(report.body.feature, "payment");
+  assert_equals(report.body.disposition, "report");
+};
+
+promise_test(async t => {
+  const report = new Promise(resolve => {
+    new ReportingObserver((reports, observer) => resolve([reports, observer]),
+                          {types: ['feature-policy']}).observe();
+  });
+  try {
+    const request = new PaymentRequest(
+      [{ supportedMethods: 'basic-card' }],
+      { total: { label: 'Total', amount: { currency: 'USD', value: 0 }}},
+      {});
+    await request.show()
+    check_report_format(await report);
+  } catch (err) {
+    // In case the show call was rejected, the report should be generated
+    // anyway. Wait for it and check the format before failing this test.
+    check_report_format(await report);
+    throw err;
+  }
+}, "PaymentRequest report only mode");
+    </script>
+  </body>
+</html>
diff --git a/third_party/WebKit/LayoutTests/external/wpt/feature-policy/reporting/payment-report-only.https.html.headers b/third_party/WebKit/LayoutTests/external/wpt/feature-policy/reporting/payment-report-only.https.html.headers
new file mode 100644
index 0000000..6411478e
--- /dev/null
+++ b/third_party/WebKit/LayoutTests/external/wpt/feature-policy/reporting/payment-report-only.https.html.headers
@@ -0,0 +1 @@
+Feature-Policy: payment-report-only 'none'
diff --git a/third_party/WebKit/LayoutTests/external/wpt/feature-policy/reporting/payment-reporting.https.html b/third_party/WebKit/LayoutTests/external/wpt/feature-policy/reporting/payment-reporting.https.html
index 03eaebe..2c1189d 100644
--- a/third_party/WebKit/LayoutTests/external/wpt/feature-policy/reporting/payment-reporting.https.html
+++ b/third_party/WebKit/LayoutTests/external/wpt/feature-policy/reporting/payment-reporting.https.html
@@ -13,6 +13,7 @@
   assert_equals(report.type, "feature-policy");
   assert_equals(report.url, document.location.href);
   assert_equals(report.body.feature, "payment");
+  assert_equals(report.body.disposition, "enforce");
   assert_equals(report.body.sourceFile, document.location.href);
   assert_equals(typeof report.body.message, "string");
   assert_equals(typeof report.body.lineNumber, "number");
diff --git a/third_party/WebKit/LayoutTests/external/wpt/feature-policy/reporting/picture-in-picture-report-only.html b/third_party/WebKit/LayoutTests/external/wpt/feature-policy/reporting/picture-in-picture-report-only.html
new file mode 100644
index 0000000..157670f
--- /dev/null
+++ b/third_party/WebKit/LayoutTests/external/wpt/feature-policy/reporting/picture-in-picture-report-only.html
@@ -0,0 +1,40 @@
+<!DOCTYPE html>
+<html>
+  <head>
+    <script src='/common/media.js'></script>
+    <script src='/resources/testharness.js'></script>
+    <script src='/resources/testharnessreport.js'></script>
+    <script src="/resources/testdriver.js"></script>
+    <script src="/resources/testdriver-vendor.js"></script>
+    <script src='../resources/picture-in-picture.js'></script>
+  </head>
+  <body>
+    <script>
+const check_report_format = ([reports, observer]) => {
+  const report = reports[0];
+  assert_equals(report.type, "feature-policy");
+  assert_equals(report.body.feature, "picture-in-picture");
+  assert_equals(report.body.disposition, "report");
+};
+
+const loadVideo = () => new Promise(resolve => {
+    const video = document.createElement('video');
+    video.src = getVideoURI('/media/movie_5');
+    video.addEventListener('loadedmetadata', () => {
+      resolve(video);
+    }, { once: true });
+});
+
+promise_pip_test(async (t) => {
+  const report = new Promise(resolve => {
+    new ReportingObserver((reports, observer) => resolve([reports, observer]),
+                          {types: ['feature-policy']}).observe();
+  });
+  const videoElement = await loadVideo();
+  await test_driver.bless('picture-in-picture');
+  await videoElement.requestPictureInPicture();
+  check_report_format(await report);
+}, "Picture-in-Picture report only mode");
+    </script>
+  </body>
+</html>
diff --git a/third_party/WebKit/LayoutTests/external/wpt/feature-policy/reporting/picture-in-picture-report-only.html.headers b/third_party/WebKit/LayoutTests/external/wpt/feature-policy/reporting/picture-in-picture-report-only.html.headers
new file mode 100644
index 0000000..0df90a3
--- /dev/null
+++ b/third_party/WebKit/LayoutTests/external/wpt/feature-policy/reporting/picture-in-picture-report-only.html.headers
@@ -0,0 +1 @@
+Feature-Policy: picture-in-picture-report-only 'none'
diff --git a/third_party/WebKit/LayoutTests/external/wpt/feature-policy/reporting/picture-in-picture-reporting.html b/third_party/WebKit/LayoutTests/external/wpt/feature-policy/reporting/picture-in-picture-reporting.html
index e3cbf10..f15f47c 100644
--- a/third_party/WebKit/LayoutTests/external/wpt/feature-policy/reporting/picture-in-picture-reporting.html
+++ b/third_party/WebKit/LayoutTests/external/wpt/feature-policy/reporting/picture-in-picture-reporting.html
@@ -15,6 +15,7 @@
   assert_equals(report.type, "feature-policy");
   assert_equals(report.url, document.location.href);
   assert_equals(report.body.feature, "picture-in-picture");
+  assert_equals(report.body.disposition, "enforce");
   assert_equals(report.body.sourceFile, document.location.href);
   assert_equals(typeof report.body.message, "string");
   assert_equals(typeof report.body.lineNumber, "number");
diff --git a/third_party/WebKit/LayoutTests/external/wpt/feature-policy/reporting/sync-xhr-report-only.html b/third_party/WebKit/LayoutTests/external/wpt/feature-policy/reporting/sync-xhr-report-only.html
new file mode 100644
index 0000000..f841f63
--- /dev/null
+++ b/third_party/WebKit/LayoutTests/external/wpt/feature-policy/reporting/sync-xhr-report-only.html
@@ -0,0 +1,28 @@
+<!DOCTYPE html>
+<html>
+  <head>
+    <script src='/resources/testharness.js'></script>
+    <script src='/resources/testharnessreport.js'></script>
+  </head>
+  <body>
+    <script>
+const check_report_format = ([reports, observer]) => {
+  const report = reports[0];
+  assert_equals(report.type, "feature-policy");
+  assert_equals(report.body.feature, "sync-xhr");
+  assert_equals(report.body.disposition, "report");
+};
+
+promise_test(async t => {
+  const report = new Promise(resolve => {
+    new ReportingObserver((reports, observer) => resolve([reports, observer]),
+                          {types: ['feature-policy']}).observe();
+  });
+  const xhr = new XMLHttpRequest();
+  xhr.open("GET", document.location.href, false);
+  xhr.send();
+  check_report_format(await report);
+}, "Sync-xhr report only mode");
+    </script>
+  </body>
+</html>
diff --git a/third_party/WebKit/LayoutTests/external/wpt/feature-policy/reporting/sync-xhr-report-only.html.headers b/third_party/WebKit/LayoutTests/external/wpt/feature-policy/reporting/sync-xhr-report-only.html.headers
new file mode 100644
index 0000000..79a82cf
--- /dev/null
+++ b/third_party/WebKit/LayoutTests/external/wpt/feature-policy/reporting/sync-xhr-report-only.html.headers
@@ -0,0 +1 @@
+Feature-Policy: sync-xhr-report-only 'none'
diff --git a/third_party/WebKit/LayoutTests/external/wpt/feature-policy/reporting/sync-xhr-reporting.html b/third_party/WebKit/LayoutTests/external/wpt/feature-policy/reporting/sync-xhr-reporting.html
index 2c76390..f16e335 100644
--- a/third_party/WebKit/LayoutTests/external/wpt/feature-policy/reporting/sync-xhr-reporting.html
+++ b/third_party/WebKit/LayoutTests/external/wpt/feature-policy/reporting/sync-xhr-reporting.html
@@ -13,6 +13,7 @@
   assert_equals(report.type, "feature-policy");
   assert_equals(report.url, document.location.href);
   assert_equals(report.body.feature, "sync-xhr");
+  assert_equals(report.body.disposition, "enforce");
   assert_equals(report.body.sourceFile, document.location.href);
   assert_equals(typeof report.body.message, "string");
   assert_equals(typeof report.body.lineNumber, "number");
diff --git a/third_party/WebKit/LayoutTests/external/wpt/feature-policy/reporting/usb-report-only.https.html b/third_party/WebKit/LayoutTests/external/wpt/feature-policy/reporting/usb-report-only.https.html
new file mode 100644
index 0000000..e44c6c5
--- /dev/null
+++ b/third_party/WebKit/LayoutTests/external/wpt/feature-policy/reporting/usb-report-only.https.html
@@ -0,0 +1,30 @@
+<!DOCTYPE html>
+<html>
+  <head>
+    <script src='/resources/testharness.js'></script>
+    <script src='/resources/testharnessreport.js'></script>
+    <script src='/resources/testdriver.js'></script>
+    <script src="/resources/testdriver-vendor.js"></script>
+  </head>
+  <body>
+    <div id='fs'></div>
+    <script>
+var check_report_format = ([reports, observer]) => {
+  let report = reports[0];
+  assert_equals(report.type, "feature-policy");
+  assert_equals(report.body.feature, "usb");
+  assert_equals(report.body.disposition, "report");
+};
+
+promise_test(async t => {
+  const report = new Promise(resolve => {
+    new ReportingObserver((reports, observer) => resolve([reports, observer]),
+                          {types: ['feature-policy']}).observe();
+  });
+  await test_driver.bless('Activate document for USB');
+  await navigator.usb.getDevices();
+  check_report_format(await report);
+}, "USB report only mode");
+    </script>
+  </body>
+</html>
diff --git a/third_party/WebKit/LayoutTests/external/wpt/feature-policy/reporting/usb-report-only.https.html.headers b/third_party/WebKit/LayoutTests/external/wpt/feature-policy/reporting/usb-report-only.https.html.headers
new file mode 100644
index 0000000..bd2e3c6
--- /dev/null
+++ b/third_party/WebKit/LayoutTests/external/wpt/feature-policy/reporting/usb-report-only.https.html.headers
@@ -0,0 +1 @@
+Feature-Policy: usb-report-only 'none'
diff --git a/third_party/WebKit/LayoutTests/external/wpt/feature-policy/reporting/usb-reporting.https.html b/third_party/WebKit/LayoutTests/external/wpt/feature-policy/reporting/usb-reporting.https.html
index f90c602e..bc4bffd 100644
--- a/third_party/WebKit/LayoutTests/external/wpt/feature-policy/reporting/usb-reporting.https.html
+++ b/third_party/WebKit/LayoutTests/external/wpt/feature-policy/reporting/usb-reporting.https.html
@@ -13,6 +13,7 @@
   assert_equals(report.type, "feature-policy");
   assert_equals(report.url, document.location.href);
   assert_equals(report.body.feature, "usb");
+  assert_equals(report.body.disposition, "enforce");
   assert_equals(report.body.sourceFile, document.location.href);
   assert_equals(typeof report.body.message, "string");
   assert_equals(typeof report.body.lineNumber, "number");
diff --git a/third_party/WebKit/LayoutTests/external/wpt/feature-policy/reporting/vr-report-only.https.html b/third_party/WebKit/LayoutTests/external/wpt/feature-policy/reporting/vr-report-only.https.html
new file mode 100644
index 0000000..91016d3
--- /dev/null
+++ b/third_party/WebKit/LayoutTests/external/wpt/feature-policy/reporting/vr-report-only.https.html
@@ -0,0 +1,31 @@
+<!DOCTYPE html>
+<html>
+  <head>
+    <script src='/resources/testharness.js'></script>
+    <script src='/resources/testharnessreport.js'></script>
+  </head>
+  <body>
+    <script>
+const check_report_format = ([reports, observer]) => {
+  const report = reports[0];
+  assert_equals(report.type, "feature-policy");
+  assert_equals(report.url, document.location.href);
+  assert_equals(report.body.feature, "vr");
+  assert_equals(report.body.disposition, "report");
+  assert_equals(report.body.sourceFile, document.location.href);
+  assert_equals(typeof report.body.message, "string");
+  assert_equals(typeof report.body.lineNumber, "number");
+  assert_equals(typeof report.body.columnNumber, "number");
+};
+
+promise_test(async t => {
+  const report = new Promise(resolve => {
+    new ReportingObserver((reports, observer) => resolve([reports, observer]),
+                          {types: ['feature-policy']}).observe();
+  });
+  await navigator.getVRDisplays();
+  check_report_format(await report);
+}, "VR report only mode");
+    </script>
+  </body>
+</html>
diff --git a/third_party/WebKit/LayoutTests/external/wpt/feature-policy/reporting/vr-report-only.https.html.headers b/third_party/WebKit/LayoutTests/external/wpt/feature-policy/reporting/vr-report-only.https.html.headers
new file mode 100644
index 0000000..b54cad2a
--- /dev/null
+++ b/third_party/WebKit/LayoutTests/external/wpt/feature-policy/reporting/vr-report-only.https.html.headers
@@ -0,0 +1 @@
+Feature-Policy: vr-report-only 'none'
diff --git a/third_party/WebKit/LayoutTests/external/wpt/feature-policy/reporting/vr-reporting.https.html b/third_party/WebKit/LayoutTests/external/wpt/feature-policy/reporting/vr-reporting.https.html
index 12cae05..9278519 100644
--- a/third_party/WebKit/LayoutTests/external/wpt/feature-policy/reporting/vr-reporting.https.html
+++ b/third_party/WebKit/LayoutTests/external/wpt/feature-policy/reporting/vr-reporting.https.html
@@ -11,6 +11,7 @@
   assert_equals(report.type, "feature-policy");
   assert_equals(report.url, document.location.href);
   assert_equals(report.body.feature, "vr");
+  assert_equals(report.body.disposition, "enforce");
   assert_equals(report.body.sourceFile, document.location.href);
   assert_equals(typeof report.body.message, "string");
   assert_equals(typeof report.body.lineNumber, "number");
diff --git a/third_party/WebKit/LayoutTests/external/wpt/feature-policy/reporting/xr-report-only.https.html b/third_party/WebKit/LayoutTests/external/wpt/feature-policy/reporting/xr-report-only.https.html
new file mode 100644
index 0000000..5d4fb06
--- /dev/null
+++ b/third_party/WebKit/LayoutTests/external/wpt/feature-policy/reporting/xr-report-only.https.html
@@ -0,0 +1,37 @@
+<!DOCTYPE html>
+<html>
+  <head>
+    <script src='/resources/testharness.js'></script>
+    <script src='/resources/testharnessreport.js'></script>
+  </head>
+  <body>
+    <script>
+const check_report_format = ([reports, observer]) => {
+  const report = reports[0];
+  assert_equals(report.type, "feature-policy");
+  assert_equals(report.url, document.location.href);
+  assert_equals(report.body.feature, "vr");
+  assert_equals(report.body.disposition, "report");
+  assert_equals(report.body.sourceFile, document.location.href);
+  assert_equals(typeof report.body.message, "string");
+  assert_equals(typeof report.body.lineNumber, "number");
+  assert_equals(typeof report.body.columnNumber, "number");
+};
+
+promise_test(async t => {
+  const report = new Promise(resolve => {
+    new ReportingObserver((reports, observer) => resolve([reports, observer]),
+                          {types: ['feature-policy']}).observe();
+  });
+  try {
+    await navigator.xr.requestDevice();
+  } catch (err) {
+    // If no XR devices are available, requestDevice() will throw NotFoundError,
+    // but the report should be generated anyway.
+    assert_equals(err.name, 'NotFoundError');
+  }
+  check_report_format(await report);
+}, "XR report only mode");
+    </script>
+  </body>
+</html>
diff --git a/third_party/WebKit/LayoutTests/external/wpt/feature-policy/reporting/xr-report-only.https.html.headers b/third_party/WebKit/LayoutTests/external/wpt/feature-policy/reporting/xr-report-only.https.html.headers
new file mode 100644
index 0000000..b54cad2a
--- /dev/null
+++ b/third_party/WebKit/LayoutTests/external/wpt/feature-policy/reporting/xr-report-only.https.html.headers
@@ -0,0 +1 @@
+Feature-Policy: vr-report-only 'none'
diff --git a/third_party/WebKit/LayoutTests/external/wpt/feature-policy/reporting/xr-reporting.https.html b/third_party/WebKit/LayoutTests/external/wpt/feature-policy/reporting/xr-reporting.https.html
index a7a1222..0c793d5b 100644
--- a/third_party/WebKit/LayoutTests/external/wpt/feature-policy/reporting/xr-reporting.https.html
+++ b/third_party/WebKit/LayoutTests/external/wpt/feature-policy/reporting/xr-reporting.https.html
@@ -11,6 +11,7 @@
   assert_equals(report.type, "feature-policy");
   assert_equals(report.url, document.location.href);
   assert_equals(report.body.feature, "vr");
+  assert_equals(report.body.disposition, "enforce");
   assert_equals(report.body.sourceFile, document.location.href);
   assert_equals(typeof report.body.message, "string");
   assert_equals(typeof report.body.lineNumber, "number");
diff --git a/third_party/blink/common/feature_policy/feature_policy.cc b/third_party/blink/common/feature_policy/feature_policy.cc
index d0a8054..1237911 100644
--- a/third_party/blink/common/feature_policy/feature_policy.cc
+++ b/third_party/blink/common/feature_policy/feature_policy.cc
@@ -34,10 +34,12 @@
     mojom::FeaturePolicyFeature feature,
     bool matches_all_origins,
     bool matches_opaque_src,
+    mojom::FeaturePolicyDisposition disposition,
     std::vector<url::Origin> origins)
     : feature(feature),
       matches_all_origins(matches_all_origins),
       matches_opaque_src(matches_opaque_src),
+      disposition(disposition),
       origins(origins) {}
 
 ParsedFeaturePolicyDeclaration::ParsedFeaturePolicyDeclaration(
@@ -56,8 +58,22 @@
   // but-not-identical allowlists, or eliminate those comparisons by maintaining
   // the allowlists in a normalized form.
   // https://crbug.com/710324
-  return std::tie(lhs.feature, lhs.matches_all_origins, lhs.origins) ==
-         std::tie(rhs.feature, rhs.matches_all_origins, rhs.origins);
+  return std::tie(lhs.feature, lhs.matches_all_origins, lhs.origins,
+                  lhs.disposition) == std::tie(rhs.feature,
+                                               rhs.matches_all_origins,
+                                               rhs.origins, rhs.disposition);
+}
+
+std::unique_ptr<ParsedFeaturePolicy> DirectivesWithDisposition(
+    mojom::FeaturePolicyDisposition disposition,
+    const ParsedFeaturePolicy& policy) {
+  std::unique_ptr<ParsedFeaturePolicy> filtered_policy =
+      std::make_unique<ParsedFeaturePolicy>();
+  for (const auto& directive : policy) {
+    if (directive.disposition == disposition)
+      filtered_policy->push_back(directive);
+  }
+  return filtered_policy;
 }
 
 FeaturePolicy::Allowlist::Allowlist() : matches_all_origins_(false) {}
diff --git a/third_party/blink/common/feature_policy/feature_policy_mojom_traits.cc b/third_party/blink/common/feature_policy/feature_policy_mojom_traits.cc
index 468be4c..d96a6b5 100644
--- a/third_party/blink/common/feature_policy/feature_policy_mojom_traits.cc
+++ b/third_party/blink/common/feature_policy/feature_policy_mojom_traits.cc
@@ -13,6 +13,7 @@
     Read(blink::mojom::ParsedFeaturePolicyDeclarationDataView in,
          blink::ParsedFeaturePolicyDeclaration* out) {
   out->matches_all_origins = in.matches_all_origins();
+  out->disposition = in.disposition();
 
   return in.ReadOrigins(&out->origins) && in.ReadFeature(&out->feature);
 }
diff --git a/third_party/blink/common/feature_policy/feature_policy_mojom_traits.h b/third_party/blink/common/feature_policy/feature_policy_mojom_traits.h
index 8f8bfb9..c0fe8ee 100644
--- a/third_party/blink/common/feature_policy/feature_policy_mojom_traits.h
+++ b/third_party/blink/common/feature_policy/feature_policy_mojom_traits.h
@@ -83,6 +83,10 @@
       const blink::ParsedFeaturePolicyDeclaration& policy) {
     return policy.matches_all_origins;
   }
+  static blink::mojom::FeaturePolicyDisposition disposition(
+      const blink::ParsedFeaturePolicyDeclaration& policy) {
+    return policy.disposition;
+  }
   static const std::vector<url::Origin>& origins(
       const blink::ParsedFeaturePolicyDeclaration& policy) {
     return policy.origins;
diff --git a/third_party/blink/common/feature_policy/feature_policy_unittest.cc b/third_party/blink/common/feature_policy/feature_policy_unittest.cc
index 7b30aec..1f8294f 100644
--- a/third_party/blink/common/feature_policy/feature_policy_unittest.cc
+++ b/third_party/blink/common/feature_policy/feature_policy_unittest.cc
@@ -143,8 +143,11 @@
       CreateFromParentPolicy(nullptr, origin_a_);
   std::unique_ptr<FeaturePolicy> policy2 =
       CreateFromParentPolicy(policy1.get(), origin_b_);
-  policy2->SetHeaderPolicy(
-      {{{kDefaultSelfFeature, false, false, {origin_b_}}}});
+  policy2->SetHeaderPolicy({{{kDefaultSelfFeature,
+                              false,
+                              false,
+                              mojom::FeaturePolicyDisposition::kEnforce,
+                              {origin_b_}}}});
   EXPECT_FALSE(policy2->IsFeatureEnabled(kDefaultSelfFeature));
 }
 
@@ -166,8 +169,11 @@
   // they are at a different origin.
   std::unique_ptr<FeaturePolicy> policy1 =
       CreateFromParentPolicy(nullptr, origin_a_);
-  policy1->SetHeaderPolicy(
-      {{{kDefaultSelfFeature, false, false, {origin_a_}}}});
+  policy1->SetHeaderPolicy({{{kDefaultSelfFeature,
+                              false,
+                              false,
+                              mojom::FeaturePolicyDisposition::kEnforce,
+                              {origin_a_}}}});
   std::unique_ptr<FeaturePolicy> policy2 =
       CreateFromParentPolicy(policy1.get(), origin_a_);
   std::unique_ptr<FeaturePolicy> policy3 =
@@ -199,8 +205,11 @@
   // it is embedded by frame 2, for which the feature is not enabled.
   std::unique_ptr<FeaturePolicy> policy1 =
       CreateFromParentPolicy(nullptr, origin_a_);
-  policy1->SetHeaderPolicy(
-      {{{kDefaultSelfFeature, false, false, {origin_a_}}}});
+  policy1->SetHeaderPolicy({{{kDefaultSelfFeature,
+                              false,
+                              false,
+                              mojom::FeaturePolicyDisposition::kEnforce,
+                              {origin_a_}}}});
   std::unique_ptr<FeaturePolicy> policy2 =
       CreateFromParentPolicy(policy1.get(), origin_b_);
   std::unique_ptr<FeaturePolicy> policy3 =
@@ -227,8 +236,11 @@
   // enabled.
   std::unique_ptr<FeaturePolicy> policy1 =
       CreateFromParentPolicy(nullptr, origin_a_);
-  policy1->SetHeaderPolicy(
-      {{{kDefaultSelfFeature, false, false, {origin_b_}}}});
+  policy1->SetHeaderPolicy({{{kDefaultSelfFeature,
+                              false,
+                              false,
+                              mojom::FeaturePolicyDisposition::kEnforce,
+                              {origin_b_}}}});
   std::unique_ptr<FeaturePolicy> policy2 =
       CreateFromParentPolicy(policy1.get(), origin_b_);
   std::unique_ptr<FeaturePolicy> policy3 =
@@ -248,8 +260,9 @@
   // Default-on feature should be disabled in top-level frame.
   std::unique_ptr<FeaturePolicy> policy1 =
       CreateFromParentPolicy(nullptr, origin_a_);
-  policy1->SetHeaderPolicy(
-      {{{kDefaultOnFeature, false, false, std::vector<url::Origin>()}}});
+  policy1->SetHeaderPolicy({{{kDefaultOnFeature, false, false,
+                              mojom::FeaturePolicyDisposition::kEnforce,
+                              std::vector<url::Origin>()}}});
   EXPECT_FALSE(policy1->IsFeatureEnabled(kDefaultOnFeature));
 }
 
@@ -265,8 +278,9 @@
   // Feature should be disabled in child frame.
   std::unique_ptr<FeaturePolicy> policy1 =
       CreateFromParentPolicy(nullptr, origin_a_);
-  policy1->SetHeaderPolicy(
-      {{{kDefaultOnFeature, false, false, std::vector<url::Origin>()}}});
+  policy1->SetHeaderPolicy({{{kDefaultOnFeature, false, false,
+                              mojom::FeaturePolicyDisposition::kEnforce,
+                              std::vector<url::Origin>()}}});
   std::unique_ptr<FeaturePolicy> policy2 =
       CreateFromParentPolicy(policy1.get(), origin_a_);
   EXPECT_FALSE(policy2->IsFeatureEnabled(kDefaultOnFeature));
@@ -286,8 +300,9 @@
       CreateFromParentPolicy(nullptr, origin_a_);
   std::unique_ptr<FeaturePolicy> policy2 =
       CreateFromParentPolicy(policy1.get(), origin_b_);
-  policy2->SetHeaderPolicy(
-      {{{kDefaultOnFeature, false, false, std::vector<url::Origin>()}}});
+  policy2->SetHeaderPolicy({{{kDefaultOnFeature, false, false,
+                              mojom::FeaturePolicyDisposition::kEnforce,
+                              std::vector<url::Origin>()}}});
   EXPECT_FALSE(policy2->IsFeatureEnabled(kDefaultOnFeature));
 }
 
@@ -310,7 +325,11 @@
       CreateFromParentPolicy(nullptr, origin_a_);
   std::unique_ptr<FeaturePolicy> policy2 =
       CreateFromParentPolicy(policy1.get(), origin_b_);
-  policy2->SetHeaderPolicy({{{kDefaultOnFeature, false, false, {origin_b_}}}});
+  policy2->SetHeaderPolicy({{{kDefaultOnFeature,
+                              false,
+                              false,
+                              mojom::FeaturePolicyDisposition::kEnforce,
+                              {origin_b_}}}});
   std::unique_ptr<FeaturePolicy> policy3 =
       CreateFromParentPolicy(policy2.get(), origin_c_);
   EXPECT_TRUE(policy2->IsFeatureEnabled(kDefaultOnFeature));
@@ -329,8 +348,9 @@
   // Default-on feature should be disabled in cross-origin child frame.
   std::unique_ptr<FeaturePolicy> policy1 =
       CreateFromParentPolicy(nullptr, origin_a_);
-  policy1->SetHeaderPolicy(
-      {{{kDefaultOnFeature, false, false, std::vector<url::Origin>()}}});
+  policy1->SetHeaderPolicy({{{kDefaultOnFeature, false, false,
+                              mojom::FeaturePolicyDisposition::kEnforce,
+                              std::vector<url::Origin>()}}});
   std::unique_ptr<FeaturePolicy> policy2 =
       CreateFromParentPolicy(policy1.get(), origin_b_);
   EXPECT_FALSE(policy2->IsFeatureEnabled(kDefaultOnFeature));
@@ -352,8 +372,9 @@
   // Feature should be enabled in top and second level; disabled in frame 3.
   std::unique_ptr<FeaturePolicy> policy1 =
       CreateFromParentPolicy(nullptr, origin_a_);
-  policy1->SetHeaderPolicy(
-      {{{kDefaultSelfFeature, true, false, std::vector<url::Origin>()}}});
+  policy1->SetHeaderPolicy({{{kDefaultSelfFeature, true, false,
+                              mojom::FeaturePolicyDisposition::kEnforce,
+                              std::vector<url::Origin>()}}});
   std::unique_ptr<FeaturePolicy> policy2 =
       CreateFromParentPolicy(policy1.get(), origin_b_);
   std::unique_ptr<FeaturePolicy> policy3 =
@@ -379,7 +400,11 @@
   // Feature should be disabled in frame 1; enabled in frames 2, 3 and 4.
   std::unique_ptr<FeaturePolicy> policy1 =
       CreateFromParentPolicy(nullptr, origin_a_);
-  policy1->SetHeaderPolicy({{{kDefaultOnFeature, false, false, {origin_b_}}}});
+  policy1->SetHeaderPolicy({{{kDefaultOnFeature,
+                              false,
+                              false,
+                              mojom::FeaturePolicyDisposition::kEnforce,
+                              {origin_b_}}}});
   std::unique_ptr<FeaturePolicy> policy2 =
       CreateFromParentPolicy(policy1.get(), origin_b_);
   std::unique_ptr<FeaturePolicy> policy3 =
@@ -408,8 +433,11 @@
   // Feature should be disabled in frames 1 and 4; enabled in frames 2 and 3.
   std::unique_ptr<FeaturePolicy> policy1 =
       CreateFromParentPolicy(nullptr, origin_a_);
-  policy1->SetHeaderPolicy(
-      {{{kDefaultSelfFeature, false, false, {origin_b_}}}});
+  policy1->SetHeaderPolicy({{{kDefaultSelfFeature,
+                              false,
+                              false,
+                              mojom::FeaturePolicyDisposition::kEnforce,
+                              {origin_b_}}}});
   std::unique_ptr<FeaturePolicy> policy2 =
       CreateFromParentPolicy(policy1.get(), origin_b_);
   std::unique_ptr<FeaturePolicy> policy3 =
@@ -438,15 +466,27 @@
   // Feature should be disabled in frames 1, 3 and 4; enabled in frame 2 only.
   std::unique_ptr<FeaturePolicy> policy1 =
       CreateFromParentPolicy(nullptr, origin_a_);
-  policy1->SetHeaderPolicy({{{kDefaultOffFeature, false, false, {origin_b_}}}});
+  policy1->SetHeaderPolicy({{{kDefaultOffFeature,
+                              false,
+                              false,
+                              mojom::FeaturePolicyDisposition::kEnforce,
+                              {origin_b_}}}});
   std::unique_ptr<FeaturePolicy> policy2 =
       CreateFromParentPolicy(policy1.get(), origin_b_);
-  policy2->SetHeaderPolicy({{{kDefaultOffFeature, false, false, {origin_b_}}}});
+  policy2->SetHeaderPolicy({{{kDefaultOffFeature,
+                              false,
+                              false,
+                              mojom::FeaturePolicyDisposition::kEnforce,
+                              {origin_b_}}}});
   std::unique_ptr<FeaturePolicy> policy3 =
       CreateFromParentPolicy(policy2.get(), origin_b_);
   std::unique_ptr<FeaturePolicy> policy4 =
       CreateFromParentPolicy(policy2.get(), origin_c_);
-  policy4->SetHeaderPolicy({{{kDefaultOffFeature, false, false, {origin_c_}}}});
+  policy4->SetHeaderPolicy({{{kDefaultOffFeature,
+                              false,
+                              false,
+                              mojom::FeaturePolicyDisposition::kEnforce,
+                              {origin_c_}}}});
   EXPECT_FALSE(policy1->IsFeatureEnabled(kDefaultOffFeature));
   EXPECT_TRUE(policy2->IsFeatureEnabled(kDefaultOffFeature));
   EXPECT_FALSE(policy3->IsFeatureEnabled(kDefaultOffFeature));
@@ -469,12 +509,14 @@
   // Feature should be enabled in all frames.
   std::unique_ptr<FeaturePolicy> policy1 =
       CreateFromParentPolicy(nullptr, origin_a_);
-  policy1->SetHeaderPolicy(
-      {{{kDefaultSelfFeature, true, false, std::vector<url::Origin>()}}});
+  policy1->SetHeaderPolicy({{{kDefaultSelfFeature, true, false,
+                              mojom::FeaturePolicyDisposition::kEnforce,
+                              std::vector<url::Origin>()}}});
   std::unique_ptr<FeaturePolicy> policy2 =
       CreateFromParentPolicy(policy1.get(), origin_b_);
-  policy2->SetHeaderPolicy(
-      {{{kDefaultSelfFeature, true, false, std::vector<url::Origin>()}}});
+  policy2->SetHeaderPolicy({{{kDefaultSelfFeature, true, false,
+                              mojom::FeaturePolicyDisposition::kEnforce,
+                              std::vector<url::Origin>()}}});
   std::unique_ptr<FeaturePolicy> policy3 =
       CreateFromParentPolicy(policy2.get(), origin_a_);
   EXPECT_TRUE(policy1->IsFeatureEnabled(kDefaultSelfFeature));
@@ -498,12 +540,16 @@
   // Feature should be enabled at the top level; disabled in all other frames.
   std::unique_ptr<FeaturePolicy> policy1 =
       CreateFromParentPolicy(nullptr, origin_a_);
-  policy1->SetHeaderPolicy(
-      {{{kDefaultSelfFeature, false, false, {origin_a_}}}});
+  policy1->SetHeaderPolicy({{{kDefaultSelfFeature,
+                              false,
+                              false,
+                              mojom::FeaturePolicyDisposition::kEnforce,
+                              {origin_a_}}}});
   std::unique_ptr<FeaturePolicy> policy2 =
       CreateFromParentPolicy(policy1.get(), origin_b_);
-  policy2->SetHeaderPolicy(
-      {{{kDefaultSelfFeature, true, false, std::vector<url::Origin>()}}});
+  policy2->SetHeaderPolicy({{{kDefaultSelfFeature, true, false,
+                              mojom::FeaturePolicyDisposition::kEnforce,
+                              std::vector<url::Origin>()}}});
   std::unique_ptr<FeaturePolicy> policy3 =
       CreateFromParentPolicy(policy2.get(), origin_a_);
   std::unique_ptr<FeaturePolicy> policy4 =
@@ -530,12 +576,18 @@
   // Feature should be enabled in all frames.
   std::unique_ptr<FeaturePolicy> policy1 =
       CreateFromParentPolicy(nullptr, origin_a_);
-  policy1->SetHeaderPolicy(
-      {{{kDefaultSelfFeature, false, false, {origin_a_, origin_b_}}}});
+  policy1->SetHeaderPolicy({{{kDefaultSelfFeature,
+                              false,
+                              false,
+                              mojom::FeaturePolicyDisposition::kEnforce,
+                              {origin_a_, origin_b_}}}});
   std::unique_ptr<FeaturePolicy> policy2 =
       CreateFromParentPolicy(policy1.get(), origin_b_);
-  policy2->SetHeaderPolicy(
-      {{{kDefaultSelfFeature, false, false, {origin_b_, origin_c_}}}});
+  policy2->SetHeaderPolicy({{{kDefaultSelfFeature,
+                              false,
+                              false,
+                              mojom::FeaturePolicyDisposition::kEnforce,
+                              {origin_b_, origin_c_}}}});
   std::unique_ptr<FeaturePolicy> policy3 =
       CreateFromParentPolicy(policy2.get(), origin_c_);
   EXPECT_TRUE(policy1->IsFeatureEnabled(kDefaultSelfFeature));
@@ -559,8 +611,11 @@
   // Feature should be enabled in frames 1, 2, and 3, and disabled in frame 4.
   std::unique_ptr<FeaturePolicy> policy1 =
       CreateFromParentPolicy(nullptr, origin_a_);
-  policy1->SetHeaderPolicy(
-      {{{kDefaultOnFeature, false, false, {origin_a_, origin_b_}}}});
+  policy1->SetHeaderPolicy({{{kDefaultOnFeature,
+                              false,
+                              false,
+                              mojom::FeaturePolicyDisposition::kEnforce,
+                              {origin_a_, origin_b_}}}});
   std::unique_ptr<FeaturePolicy> policy2 =
       CreateFromParentPolicy(policy1.get(), origin_b_);
   std::unique_ptr<FeaturePolicy> policy3 =
@@ -590,8 +645,11 @@
   // 4.
   std::unique_ptr<FeaturePolicy> policy1 =
       CreateFromParentPolicy(nullptr, origin_a_);
-  policy1->SetHeaderPolicy(
-      {{{kDefaultSelfFeature, false, false, {origin_a_, origin_b_}}}});
+  policy1->SetHeaderPolicy({{{kDefaultSelfFeature,
+                              false,
+                              false,
+                              mojom::FeaturePolicyDisposition::kEnforce,
+                              {origin_a_, origin_b_}}}});
   std::unique_ptr<FeaturePolicy> policy2 =
       CreateFromParentPolicy(policy1.get(), origin_b_);
   std::unique_ptr<FeaturePolicy> policy3 =
@@ -623,14 +681,24 @@
   // should be enabled in frame 1, and disabled in frames 2 and 3.
   std::unique_ptr<FeaturePolicy> policy1 =
       CreateFromParentPolicy(nullptr, origin_a_);
-  policy1->SetHeaderPolicy(
-      {{{kDefaultSelfFeature, false, false, {origin_a_, origin_b_}},
-        {kDefaultOnFeature, false, false, {origin_a_}}}});
+  policy1->SetHeaderPolicy({{{kDefaultSelfFeature,
+                              false,
+                              false,
+                              mojom::FeaturePolicyDisposition::kEnforce,
+                              {origin_a_, origin_b_}},
+                             {kDefaultOnFeature,
+                              false,
+                              false,
+                              mojom::FeaturePolicyDisposition::kEnforce,
+                              {origin_a_}}}});
   std::unique_ptr<FeaturePolicy> policy2 =
       CreateFromParentPolicy(policy1.get(), origin_b_);
   policy2->SetHeaderPolicy(
-      {{{kDefaultSelfFeature, true, false, std::vector<url::Origin>()},
-        {kDefaultOnFeature, true, false, std::vector<url::Origin>()}}});
+      {{{kDefaultSelfFeature, true, false,
+         mojom::FeaturePolicyDisposition::kEnforce, std::vector<url::Origin>()},
+        {kDefaultOnFeature, true, false,
+         mojom::FeaturePolicyDisposition::kEnforce,
+         std::vector<url::Origin>()}}});
   std::unique_ptr<FeaturePolicy> policy3 =
       CreateFromParentPolicy(policy2.get(), origin_c_);
   EXPECT_TRUE(policy1->IsFeatureEnabled(kDefaultSelfFeature));
@@ -650,8 +718,11 @@
   // and disabled for origin C.
   std::unique_ptr<FeaturePolicy> policy1 =
       CreateFromParentPolicy(nullptr, origin_a_);
-  policy1->SetHeaderPolicy(
-      {{{kDefaultOffFeature, false, false, {origin_a_, origin_b_}}}});
+  policy1->SetHeaderPolicy({{{kDefaultOffFeature,
+                              false,
+                              false,
+                              mojom::FeaturePolicyDisposition::kEnforce,
+                              {origin_a_, origin_b_}}}});
   EXPECT_TRUE(
       policy1->IsFeatureEnabledForOrigin(kDefaultOffFeature, origin_a_));
   EXPECT_TRUE(
@@ -680,7 +751,11 @@
   std::unique_ptr<FeaturePolicy> policy1 =
       CreateFromParentPolicy(nullptr, origin_a_);
   ParsedFeaturePolicy frame_policy = {
-      {{kDefaultSelfFeature, false, false, {origin_b_}}}};
+      {{kDefaultSelfFeature,
+        false,
+        false,
+        mojom::FeaturePolicyDisposition::kEnforce,
+        {origin_b_}}}};
   std::unique_ptr<FeaturePolicy> policy2 =
       CreateFromParentWithFramePolicy(policy1.get(), frame_policy, origin_b_);
   EXPECT_TRUE(
@@ -715,7 +790,9 @@
   std::unique_ptr<FeaturePolicy> policy1 =
       CreateFromParentPolicy(nullptr, origin_a_);
   ParsedFeaturePolicy frame_policy = {
-      {{kDefaultSelfFeature, true, false, std::vector<url::Origin>()}}};
+      {{kDefaultSelfFeature, true, false,
+        mojom::FeaturePolicyDisposition::kEnforce,
+        std::vector<url::Origin>()}}};
   std::unique_ptr<FeaturePolicy> policy2 =
       CreateFromParentWithFramePolicy(policy1.get(), frame_policy, origin_b_);
   EXPECT_TRUE(
@@ -761,11 +838,19 @@
   std::unique_ptr<FeaturePolicy> policy1 =
       CreateFromParentPolicy(nullptr, origin_a_);
   ParsedFeaturePolicy frame_policy1 = {
-      {{kDefaultSelfFeature, false, false, {origin_b_}}}};
+      {{kDefaultSelfFeature,
+        false,
+        false,
+        mojom::FeaturePolicyDisposition::kEnforce,
+        {origin_b_}}}};
   std::unique_ptr<FeaturePolicy> policy2 =
       CreateFromParentWithFramePolicy(policy1.get(), frame_policy1, origin_b_);
   ParsedFeaturePolicy frame_policy2 = {
-      {{kDefaultSelfFeature, false, false, {origin_c_}}}};
+      {{kDefaultSelfFeature,
+        false,
+        false,
+        mojom::FeaturePolicyDisposition::kEnforce,
+        {origin_c_}}}};
   std::unique_ptr<FeaturePolicy> policy3 =
       CreateFromParentWithFramePolicy(policy2.get(), frame_policy2, origin_c_);
   std::unique_ptr<FeaturePolicy> policy4 =
@@ -806,11 +891,15 @@
   std::unique_ptr<FeaturePolicy> policy1 =
       CreateFromParentPolicy(nullptr, origin_a_);
   ParsedFeaturePolicy frame_policy1 = {
-      {{kDefaultOnFeature, false, false, std::vector<url::Origin>()}}};
+      {{kDefaultOnFeature, false, false,
+        mojom::FeaturePolicyDisposition::kEnforce,
+        std::vector<url::Origin>()}}};
   std::unique_ptr<FeaturePolicy> policy2 =
       CreateFromParentWithFramePolicy(policy1.get(), frame_policy1, origin_a_);
   ParsedFeaturePolicy frame_policy2 = {
-      {{kDefaultOnFeature, false, false, std::vector<url::Origin>()}}};
+      {{kDefaultOnFeature, false, false,
+        mojom::FeaturePolicyDisposition::kEnforce,
+        std::vector<url::Origin>()}}};
   std::unique_ptr<FeaturePolicy> policy3 =
       CreateFromParentWithFramePolicy(policy1.get(), frame_policy2, origin_b_);
   EXPECT_TRUE(policy1->IsFeatureEnabledForOrigin(kDefaultOnFeature, origin_a_));
@@ -851,13 +940,25 @@
   // child frames because they did not declare their own policy to enable it.
   std::unique_ptr<FeaturePolicy> policy1 =
       CreateFromParentPolicy(nullptr, origin_a_);
-  policy1->SetHeaderPolicy({{{kDefaultOffFeature, false, false, {origin_a_}}}});
+  policy1->SetHeaderPolicy({{{kDefaultOffFeature,
+                              false,
+                              false,
+                              mojom::FeaturePolicyDisposition::kEnforce,
+                              {origin_a_}}}});
   ParsedFeaturePolicy frame_policy1 = {
-      {{kDefaultOffFeature, false, false, {origin_a_}}}};
+      {{kDefaultOffFeature,
+        false,
+        false,
+        mojom::FeaturePolicyDisposition::kEnforce,
+        {origin_a_}}}};
   std::unique_ptr<FeaturePolicy> policy2 =
       CreateFromParentWithFramePolicy(policy1.get(), frame_policy1, origin_a_);
   ParsedFeaturePolicy frame_policy2 = {
-      {{kDefaultOffFeature, false, false, {origin_b_}}}};
+      {{kDefaultOffFeature,
+        false,
+        false,
+        mojom::FeaturePolicyDisposition::kEnforce,
+        {origin_b_}}}};
   std::unique_ptr<FeaturePolicy> policy3 =
       CreateFromParentWithFramePolicy(policy1.get(), frame_policy2, origin_b_);
   EXPECT_TRUE(
@@ -902,17 +1003,37 @@
   // they declare their own policy to enable it.
   std::unique_ptr<FeaturePolicy> policy1 =
       CreateFromParentPolicy(nullptr, origin_a_);
-  policy1->SetHeaderPolicy({{{kDefaultOffFeature, false, false, {origin_a_}}}});
+  policy1->SetHeaderPolicy({{{kDefaultOffFeature,
+                              false,
+                              false,
+                              mojom::FeaturePolicyDisposition::kEnforce,
+                              {origin_a_}}}});
   ParsedFeaturePolicy frame_policy1 = {
-      {{kDefaultOffFeature, false, false, {origin_a_}}}};
+      {{kDefaultOffFeature,
+        false,
+        false,
+        mojom::FeaturePolicyDisposition::kEnforce,
+        {origin_a_}}}};
   std::unique_ptr<FeaturePolicy> policy2 =
       CreateFromParentWithFramePolicy(policy1.get(), frame_policy1, origin_a_);
-  policy2->SetHeaderPolicy({{{kDefaultOffFeature, false, false, {origin_a_}}}});
+  policy2->SetHeaderPolicy({{{kDefaultOffFeature,
+                              false,
+                              false,
+                              mojom::FeaturePolicyDisposition::kEnforce,
+                              {origin_a_}}}});
   ParsedFeaturePolicy frame_policy2 = {
-      {{kDefaultOffFeature, false, false, {origin_b_}}}};
+      {{kDefaultOffFeature,
+        false,
+        false,
+        mojom::FeaturePolicyDisposition::kEnforce,
+        {origin_b_}}}};
   std::unique_ptr<FeaturePolicy> policy3 =
       CreateFromParentWithFramePolicy(policy1.get(), frame_policy2, origin_b_);
-  policy3->SetHeaderPolicy({{{kDefaultOffFeature, false, false, {origin_b_}}}});
+  policy3->SetHeaderPolicy({{{kDefaultOffFeature,
+                              false,
+                              false,
+                              mojom::FeaturePolicyDisposition::kEnforce,
+                              {origin_b_}}}});
   EXPECT_TRUE(
       policy1->IsFeatureEnabledForOrigin(kDefaultOffFeature, origin_a_));
   EXPECT_FALSE(
@@ -956,18 +1077,28 @@
   // policy.
   std::unique_ptr<FeaturePolicy> policy1 =
       CreateFromParentPolicy(nullptr, origin_a_);
-  policy1->SetHeaderPolicy(
-      {{{kDefaultSelfFeature, false, false, {origin_a_, origin_b_}}}});
+  policy1->SetHeaderPolicy({{{kDefaultSelfFeature,
+                              false,
+                              false,
+                              mojom::FeaturePolicyDisposition::kEnforce,
+                              {origin_a_, origin_b_}}}});
   ParsedFeaturePolicy frame_policy1 = {
-      {{kDefaultSelfFeature, false, false, std::vector<url::Origin>()}}};
+      {{kDefaultSelfFeature, false, false,
+        mojom::FeaturePolicyDisposition::kEnforce,
+        std::vector<url::Origin>()}}};
   std::unique_ptr<FeaturePolicy> policy2 =
       CreateFromParentWithFramePolicy(policy1.get(), frame_policy1, origin_b_);
   ParsedFeaturePolicy frame_policy2 = {
-      {{kDefaultSelfFeature, false, false, std::vector<url::Origin>()}}};
+      {{kDefaultSelfFeature, false, false,
+        mojom::FeaturePolicyDisposition::kEnforce,
+        std::vector<url::Origin>()}}};
   std::unique_ptr<FeaturePolicy> policy3 =
       CreateFromParentWithFramePolicy(policy1.get(), frame_policy2, origin_b_);
-  policy3->SetHeaderPolicy(
-      {{{kDefaultSelfFeature, false, false, {origin_b_}}}});
+  policy3->SetHeaderPolicy({{{kDefaultSelfFeature,
+                              false,
+                              false,
+                              mojom::FeaturePolicyDisposition::kEnforce,
+                              {origin_b_}}}});
   EXPECT_FALSE(
       policy2->IsFeatureEnabledForOrigin(kDefaultSelfFeature, origin_b_));
   EXPECT_FALSE(
@@ -1002,13 +1133,20 @@
   std::unique_ptr<FeaturePolicy> policy1 =
       CreateFromParentPolicy(nullptr, origin_a_);
   ParsedFeaturePolicy frame_policy1 = {
-      {{kDefaultSelfFeature, false, false, {origin_b_}}}};
+      {{kDefaultSelfFeature,
+        false,
+        false,
+        mojom::FeaturePolicyDisposition::kEnforce,
+        {origin_b_}}}};
   std::unique_ptr<FeaturePolicy> policy2 =
       CreateFromParentWithFramePolicy(policy1.get(), frame_policy1, origin_b_);
-  policy2->SetHeaderPolicy(
-      {{{kDefaultSelfFeature, true, false, std::vector<url::Origin>()}}});
+  policy2->SetHeaderPolicy({{{kDefaultSelfFeature, true, false,
+                              mojom::FeaturePolicyDisposition::kEnforce,
+                              std::vector<url::Origin>()}}});
   ParsedFeaturePolicy frame_policy2 = {
-      {{kDefaultSelfFeature, false, false, std::vector<url::Origin>()}}};
+      {{kDefaultSelfFeature, false, false,
+        mojom::FeaturePolicyDisposition::kEnforce,
+        std::vector<url::Origin>()}}};
   std::unique_ptr<FeaturePolicy> policy3 =
       CreateFromParentWithFramePolicy(policy2.get(), frame_policy2, origin_c_);
   std::unique_ptr<FeaturePolicy> policy4 =
@@ -1043,14 +1181,21 @@
   // Default-self feature should be disabled in all frames.
   std::unique_ptr<FeaturePolicy> policy1 =
       CreateFromParentPolicy(nullptr, origin_a_);
-  policy1->SetHeaderPolicy(
-      {{{kDefaultSelfFeature, false, false, std::vector<url::Origin>()}}});
+  policy1->SetHeaderPolicy({{{kDefaultSelfFeature, false, false,
+                              mojom::FeaturePolicyDisposition::kEnforce,
+                              std::vector<url::Origin>()}}});
   ParsedFeaturePolicy frame_policy1 = {
-      {{kDefaultSelfFeature, false, false, {origin_b_}}}};
+      {{kDefaultSelfFeature,
+        false,
+        false,
+        mojom::FeaturePolicyDisposition::kEnforce,
+        {origin_b_}}}};
   std::unique_ptr<FeaturePolicy> policy2 =
       CreateFromParentWithFramePolicy(policy1.get(), frame_policy1, origin_b_);
   ParsedFeaturePolicy frame_policy2 = {
-      {{kDefaultSelfFeature, true, false, std::vector<url::Origin>()}}};
+      {{kDefaultSelfFeature, true, false,
+        mojom::FeaturePolicyDisposition::kEnforce,
+        std::vector<url::Origin>()}}};
   std::unique_ptr<FeaturePolicy> policy3 =
       CreateFromParentWithFramePolicy(policy1.get(), frame_policy2, origin_a_);
   EXPECT_FALSE(
@@ -1088,17 +1233,31 @@
   // enabled in the remaining frames.
   std::unique_ptr<FeaturePolicy> policy1 =
       CreateFromParentPolicy(nullptr, origin_a_);
-  policy1->SetHeaderPolicy({{kDefaultSelfFeature, false, false, {origin_b_}}});
+  policy1->SetHeaderPolicy({{{kDefaultSelfFeature,
+                              false,
+                              false,
+                              mojom::FeaturePolicyDisposition::kEnforce,
+                              {origin_b_}}}});
   ParsedFeaturePolicy frame_policy1 = {
-      {{kDefaultSelfFeature, false, false, {origin_a_}}}};
+      {{kDefaultSelfFeature,
+        false,
+        false,
+        mojom::FeaturePolicyDisposition::kEnforce,
+        {origin_a_}}}};
   std::unique_ptr<FeaturePolicy> policy2 =
       CreateFromParentWithFramePolicy(policy1.get(), frame_policy1, origin_b_);
   ParsedFeaturePolicy frame_policy2 = {
-      {{kDefaultSelfFeature, false, false, {origin_b_}}}};
+      {{kDefaultSelfFeature,
+        false,
+        false,
+        mojom::FeaturePolicyDisposition::kEnforce,
+        {origin_b_}}}};
   std::unique_ptr<FeaturePolicy> policy3 =
       CreateFromParentWithFramePolicy(policy1.get(), frame_policy2, origin_b_);
   ParsedFeaturePolicy frame_policy3 = {
-      {{kDefaultSelfFeature, true, false, std::vector<url::Origin>()}}};
+      {{kDefaultSelfFeature, true, false,
+        mojom::FeaturePolicyDisposition::kEnforce,
+        std::vector<url::Origin>()}}};
   std::unique_ptr<FeaturePolicy> policy4 =
       CreateFromParentWithFramePolicy(policy1.get(), frame_policy3, origin_b_);
   EXPECT_FALSE(
@@ -1164,7 +1323,9 @@
       CreateFromParentPolicy(nullptr, origin_a_);
   url::Origin sandboxed_origin = url::Origin();
   ParsedFeaturePolicy frame_policy = {
-      {{kDefaultSelfFeature, true, false, std::vector<url::Origin>()}}};
+      {{kDefaultSelfFeature, true, false,
+        mojom::FeaturePolicyDisposition::kEnforce,
+        std::vector<url::Origin>()}}};
   std::unique_ptr<FeaturePolicy> policy2 = CreateFromParentWithFramePolicy(
       policy1.get(), frame_policy, sandboxed_origin);
   EXPECT_TRUE(policy2->IsFeatureEnabledForOrigin(kDefaultOnFeature, origin_a_));
@@ -1195,7 +1356,9 @@
       CreateFromParentPolicy(nullptr, origin_a_);
   url::Origin sandboxed_origin = url::Origin();
   ParsedFeaturePolicy frame_policy = {
-      {{kDefaultSelfFeature, false, true, std::vector<url::Origin>()}}};
+      {{kDefaultSelfFeature, false, true,
+        mojom::FeaturePolicyDisposition::kEnforce,
+        std::vector<url::Origin>()}}};
   std::unique_ptr<FeaturePolicy> policy2 = CreateFromParentWithFramePolicy(
       policy1.get(), frame_policy, sandboxed_origin);
   EXPECT_TRUE(policy2->IsFeatureEnabledForOrigin(kDefaultOnFeature, origin_a_));
@@ -1223,11 +1386,14 @@
   // However, it will not pass that on to any other origin
   std::unique_ptr<FeaturePolicy> policy1 =
       CreateFromParentPolicy(nullptr, origin_a_);
-  policy1->SetHeaderPolicy(
-      {{{kDefaultSelfFeature, true, false, std::vector<url::Origin>()}}});
+  policy1->SetHeaderPolicy({{{kDefaultSelfFeature, true, false,
+                              mojom::FeaturePolicyDisposition::kEnforce,
+                              std::vector<url::Origin>()}}});
   url::Origin sandboxed_origin = url::Origin();
   ParsedFeaturePolicy frame_policy = {
-      {{kDefaultSelfFeature, false, true, std::vector<url::Origin>()}}};
+      {{kDefaultSelfFeature, false, true,
+        mojom::FeaturePolicyDisposition::kEnforce,
+        std::vector<url::Origin>()}}};
   std::unique_ptr<FeaturePolicy> policy2 = CreateFromParentWithFramePolicy(
       policy1.get(), frame_policy, sandboxed_origin);
   EXPECT_TRUE(policy2->IsFeatureEnabled(kDefaultSelfFeature));
@@ -1260,7 +1426,9 @@
   url::Origin sandboxed_origin_1 = url::Origin();
   url::Origin sandboxed_origin_2 = url::Origin();
   ParsedFeaturePolicy frame_policy = {
-      {{kDefaultSelfFeature, true, false, std::vector<url::Origin>()}}};
+      {{kDefaultSelfFeature, true, false,
+        mojom::FeaturePolicyDisposition::kEnforce,
+        std::vector<url::Origin>()}}};
   std::unique_ptr<FeaturePolicy> policy2 = CreateFromParentWithFramePolicy(
       policy1.get(), frame_policy, sandboxed_origin_1);
   std::unique_ptr<FeaturePolicy> policy3 =
@@ -1304,11 +1472,15 @@
   url::Origin sandboxed_origin_1 = origin_a_.DeriveNewOpaqueOrigin();
   url::Origin sandboxed_origin_2 = sandboxed_origin_1.DeriveNewOpaqueOrigin();
   ParsedFeaturePolicy frame_policy_1 = {
-      {{kDefaultSelfFeature, true, false, std::vector<url::Origin>()}}};
+      {{kDefaultSelfFeature, true, false,
+        mojom::FeaturePolicyDisposition::kEnforce,
+        std::vector<url::Origin>()}}};
   std::unique_ptr<FeaturePolicy> policy2 = CreateFromParentWithFramePolicy(
       policy1.get(), frame_policy_1, sandboxed_origin_1);
   ParsedFeaturePolicy frame_policy_2 = {
-      {{kDefaultSelfFeature, true, false, std::vector<url::Origin>()}}};
+      {{kDefaultSelfFeature, true, false,
+        mojom::FeaturePolicyDisposition::kEnforce,
+        std::vector<url::Origin>()}}};
   std::unique_ptr<FeaturePolicy> policy3 = CreateFromParentWithFramePolicy(
       policy2.get(), frame_policy_2, sandboxed_origin_2);
   EXPECT_TRUE(policy3->IsFeatureEnabledForOrigin(kDefaultOnFeature, origin_a_));
@@ -1340,8 +1512,10 @@
       CreateFromParentPolicy(nullptr, origin_a_);
   ParsedFeaturePolicy frame_policy = {
       {{mojom::FeaturePolicyFeature::kNotFound, false, true,
-        std::vector<url::Origin>()},
-       {kUnavailableFeature, false, true, std::vector<url::Origin>()}}};
+        mojom::FeaturePolicyDisposition::kEnforce, std::vector<url::Origin>()},
+       {kUnavailableFeature, false, true,
+        mojom::FeaturePolicyDisposition::kEnforce,
+        std::vector<url::Origin>()}}};
   std::unique_ptr<FeaturePolicy> policy2 =
       CreateFromParentWithFramePolicy(policy1.get(), frame_policy, origin_b_);
   EXPECT_FALSE(PolicyContainsInheritedValue(
@@ -1354,4 +1528,19 @@
       PolicyContainsInheritedValue(policy2.get(), kUnavailableFeature));
 }
 
+TEST_F(FeaturePolicyTest, TestReportOnlyFeaturesIncludedInHeader) {
+  // +---------------------------------------------------+
+  // |(1)Origin A                                        |
+  // |Feature-Policy: default-self-report-only 'none'    |
+  // +---------------------------------------------------+
+  // A feature which is tagged as '-report-only' should be included in the
+  // reporting policy.
+  std::unique_ptr<FeaturePolicy> policy1 =
+      CreateFromParentPolicy(nullptr, origin_a_);
+  policy1->SetHeaderPolicy({{{kDefaultSelfFeature, false, false,
+                              mojom::FeaturePolicyDisposition::kReport,
+                              std::vector<url::Origin>()}}});
+  EXPECT_FALSE(policy1->IsFeatureEnabled(kDefaultSelfFeature));
+}
+
 }  // namespace blink
diff --git a/third_party/blink/public/common/feature_policy/feature_policy.h b/third_party/blink/public/common/feature_policy/feature_policy.h
index f90a7ab..e929f47 100644
--- a/third_party/blink/public/common/feature_policy/feature_policy.h
+++ b/third_party/blink/public/common/feature_policy/feature_policy.h
@@ -92,6 +92,7 @@
   ParsedFeaturePolicyDeclaration(mojom::FeaturePolicyFeature feature,
                                  bool matches_all_origins,
                                  bool matches_opaque_src,
+                                 mojom::FeaturePolicyDisposition disposition,
                                  std::vector<url::Origin> origins);
   ParsedFeaturePolicyDeclaration(const ParsedFeaturePolicyDeclaration& rhs);
   ParsedFeaturePolicyDeclaration& operator=(
@@ -106,6 +107,7 @@
   // of the iframe to be present in |origins|, but for sandboxed iframes, this
   // flag is set instead.
   bool matches_opaque_src;
+  mojom::FeaturePolicyDisposition disposition;
   std::vector<url::Origin> origins;
 };
 
@@ -114,6 +116,13 @@
 bool BLINK_COMMON_EXPORT operator==(const ParsedFeaturePolicyDeclaration& lhs,
                                     const ParsedFeaturePolicyDeclaration& rhs);
 
+// ParsedFeaturePolicy objects can contain directives of both enforcing and
+// report-only dispositions. This utility function will extract just the items
+// of one disposition or the other.
+BLINK_COMMON_EXPORT std::unique_ptr<ParsedFeaturePolicy>
+DirectivesWithDisposition(mojom::FeaturePolicyDisposition disposition,
+                          const ParsedFeaturePolicy& policy);
+
 class BLINK_COMMON_EXPORT FeaturePolicy {
  public:
   // Represents a collection of origins which make up an allowlist in a feature
diff --git a/third_party/blink/public/mojom/feature_policy/feature_policy.mojom b/third_party/blink/public/mojom/feature_policy/feature_policy.mojom
index 508e0c03..63d4b44 100644
--- a/third_party/blink/public/mojom/feature_policy/feature_policy.mojom
+++ b/third_party/blink/public/mojom/feature_policy/feature_policy.mojom
@@ -105,6 +105,13 @@
   // chromium/src/tools/metrics/histograms/ to update the UMA mapping.
 };
 
+// This enum is used to distinguish between report-only directives and enforcing
+// directives.
+enum FeaturePolicyDisposition {
+  kEnforce,
+  kReport,
+};
+
 // This struct holds feature policy allowlist data that needs to be replicated
 // between a RenderFrame and any of its associated RenderFrameProxies. A list of
 // these form a ParsedFeaturePolicy.
@@ -112,5 +119,6 @@
 struct ParsedFeaturePolicyDeclaration {
   FeaturePolicyFeature feature;
   bool matches_all_origins;
+  FeaturePolicyDisposition disposition;
   array<url.mojom.Origin> origins;
 };
diff --git a/third_party/blink/public/platform/reporting.mojom b/third_party/blink/public/platform/reporting.mojom
index c9e9127..f1da9a34 100644
--- a/third_party/blink/public/platform/reporting.mojom
+++ b/third_party/blink/public/platform/reporting.mojom
@@ -51,6 +51,7 @@
   // (See //third_party/blink/renderer/core/frame/feature_policy_violation_report_body.h.)
   QueueFeaturePolicyViolationReport(url.mojom.Url url,
                                     string policy,
+                                    string disposition,
                                     string message,
                                     string? source_file,
                                     int32 line_number,
diff --git a/third_party/blink/renderer/core/dom/document.cc b/third_party/blink/renderer/core/dom/document.cc
index 227799f..196a751 100644
--- a/third_party/blink/renderer/core/dom/document.cc
+++ b/third_party/blink/renderer/core/dom/document.cc
@@ -7676,8 +7676,10 @@
   return *lazy_load_image_observer_;
 }
 
-void Document::ReportFeaturePolicyViolation(mojom::FeaturePolicyFeature feature,
-                                            const String& message) const {
+void Document::ReportFeaturePolicyViolation(
+    mojom::FeaturePolicyFeature feature,
+    mojom::FeaturePolicyDisposition disposition,
+    const String& message) const {
   if (!RuntimeEnabledFeatures::FeaturePolicyReportingEnabled())
     return;
   LocalFrame* frame = GetFrame();
@@ -7685,7 +7687,10 @@
     return;
   const String& feature_name = GetNameForFeature(feature);
   FeaturePolicyViolationReportBody* body = new FeaturePolicyViolationReportBody(
-      feature_name, "Feature policy violation", SourceLocation::Capture());
+      feature_name, "Feature policy violation",
+      (disposition == mojom::FeaturePolicyDisposition::kReport ? "report"
+                                                               : "enforce"),
+      SourceLocation::Capture());
   Report* report = new Report("feature-policy", Url().GetString(), body);
   ReportingContext::From(this)->QueueReport(report);
 
@@ -7697,13 +7702,19 @@
 
   // Send the feature policy violation report to the Reporting API.
   frame->GetReportingService()->QueueFeaturePolicyViolationReport(
-      Url(), feature_name, "Feature policy violation", body->sourceFile(),
-      line_number, column_number);
-  frame->Console().AddMessage(ConsoleMessage::Create(
-      kViolationMessageSource, kErrorMessageLevel,
-      (message.IsEmpty() ? ("Feature policy violation: " + feature_name +
-                            " is not allowed in this document.")
-                         : message)));
+      Url(), feature_name,
+      (disposition == mojom::FeaturePolicyDisposition::kReport ? "report"
+                                                               : "enforce"),
+      "Feature policy violation", body->sourceFile(), line_number,
+      column_number);
+  // TODO(iclelland): Report something different in report-only mode
+  if (disposition == mojom::FeaturePolicyDisposition::kEnforce) {
+    frame->Console().AddMessage(ConsoleMessage::Create(
+        kViolationMessageSource, kErrorMessageLevel,
+        (message.IsEmpty() ? ("Feature policy violation: " + feature_name +
+                              " is not allowed in this document.")
+                           : message)));
+  }
 }
 
 void Document::IncrementNumberOfCanvases() {
diff --git a/third_party/blink/renderer/core/dom/document.h b/third_party/blink/renderer/core/dom/document.h
index f1d89465..f2f5a59ff 100644
--- a/third_party/blink/renderer/core/dom/document.h
+++ b/third_party/blink/renderer/core/dom/document.h
@@ -1488,6 +1488,7 @@
 
   void ReportFeaturePolicyViolation(
       mojom::FeaturePolicyFeature,
+      mojom::FeaturePolicyDisposition,
       const String& message = g_empty_string) const override;
 
   bool IsParsedFeaturePolicy(mojom::FeaturePolicyFeature feature) const {
diff --git a/third_party/blink/renderer/core/execution_context/security_context.cc b/third_party/blink/renderer/core/execution_context/security_context.cc
index 60748fa6f..28651e7 100644
--- a/third_party/blink/renderer/core/execution_context/security_context.cc
+++ b/third_party/blink/renderer/core/execution_context/security_context.cc
@@ -111,32 +111,70 @@
   feature_policy_ = std::move(feature_policy);
 }
 
+// Uses the parent enforcing policy; parsed_header and container_policy can
+// both contain report-only directives, which will be used to construct the
+// report-only policy for this context.
 void SecurityContext::InitializeFeaturePolicy(
     const ParsedFeaturePolicy& parsed_header,
     const ParsedFeaturePolicy& container_policy,
     const FeaturePolicy* parent_feature_policy) {
+  report_only_feature_policy_ = nullptr;
   if (!HasCustomizedFeaturePolicy()) {
     feature_policy_ = FeaturePolicy::CreateFromParentPolicy(
         nullptr, {}, security_origin_->ToUrlOrigin());
     return;
   }
+
   feature_policy_ = FeaturePolicy::CreateFromParentPolicy(
-      parent_feature_policy, container_policy, security_origin_->ToUrlOrigin());
-  feature_policy_->SetHeaderPolicy(parsed_header);
+      parent_feature_policy,
+      *DirectivesWithDisposition(mojom::FeaturePolicyDisposition::kEnforce,
+                                 container_policy),
+      security_origin_->ToUrlOrigin());
+  feature_policy_->SetHeaderPolicy(*DirectivesWithDisposition(
+      mojom::FeaturePolicyDisposition::kEnforce, parsed_header));
+  if (RuntimeEnabledFeatures::FeaturePolicyReportingEnabled()) {
+    report_only_feature_policy_ = FeaturePolicy::CreateFromParentPolicy(
+        parent_feature_policy,
+        *DirectivesWithDisposition(mojom::FeaturePolicyDisposition::kReport,
+                                   container_policy),
+        security_origin_->ToUrlOrigin());
+    report_only_feature_policy_->SetHeaderPolicy(*DirectivesWithDisposition(
+        mojom::FeaturePolicyDisposition::kReport, parsed_header));
+  }
 }
 
 bool SecurityContext::IsFeatureEnabled(mojom::FeaturePolicyFeature feature,
                                        ReportOptions report_on_failure,
                                        const String& message) const {
+  FeatureEnabledState state = GetFeatureEnabledState(feature);
+  if (state == FeatureEnabledState::kEnabled)
+    return true;
+  if (report_on_failure == ReportOptions::kReportOnFailure &&
+      RuntimeEnabledFeatures::FeaturePolicyReportingEnabled()) {
+    ReportFeaturePolicyViolation(
+        feature,
+        (state == FeatureEnabledState::kReportOnly
+             ? mojom::FeaturePolicyDisposition::kReport
+             : mojom::FeaturePolicyDisposition::kEnforce),
+        message);
+  }
+  return (state != FeatureEnabledState::kDisabled);
+}
+
+FeatureEnabledState SecurityContext::GetFeatureEnabledState(
+    mojom::FeaturePolicyFeature feature) const {
   // The policy should always be initialized before checking it to ensure we
   // properly inherit the parent policy.
   DCHECK(feature_policy_);
 
-  if (feature_policy_->IsFeatureEnabled(feature))
-    return true;
-  if (report_on_failure == ReportOptions::kReportOnFailure)
-    ReportFeaturePolicyViolation(feature, message);
-  return false;
+  if (feature_policy_->IsFeatureEnabled(feature)) {
+    if (report_only_feature_policy_ &&
+        !report_only_feature_policy_->IsFeatureEnabled(feature)) {
+      return FeatureEnabledState::kReportOnly;
+    }
+    return FeatureEnabledState::kEnabled;
+  }
+  return FeatureEnabledState::kDisabled;
 }
 
 }  // namespace blink
diff --git a/third_party/blink/renderer/core/execution_context/security_context.h b/third_party/blink/renderer/core/execution_context/security_context.h
index 52347473d..0257f3e 100644
--- a/third_party/blink/renderer/core/execution_context/security_context.h
+++ b/third_party/blink/renderer/core/execution_context/security_context.h
@@ -51,8 +51,10 @@
 // Whether to report policy violations when checking whether a feature is
 // enabled.
 enum class ReportOptions { kReportOnFailure, kDoNotReport };
+enum class FeatureEnabledState { kDisabled, kReportOnly, kEnabled };
 
 namespace mojom {
+enum class FeaturePolicyDisposition : int32_t;
 enum class FeaturePolicyFeature : int32_t;
 enum class IPAddressSpace : int32_t;
 }
@@ -124,6 +126,9 @@
   }
 
   FeaturePolicy* GetFeaturePolicy() const { return feature_policy_.get(); }
+  FeaturePolicy* GetReportOnlyFeaturePolicy() const {
+    return report_only_feature_policy_.get();
+  }
   void SetFeaturePolicy(std::unique_ptr<FeaturePolicy> feature_policy);
   void InitializeFeaturePolicy(const ParsedFeaturePolicy& parsed_header,
                                const ParsedFeaturePolicy& container_policy,
@@ -138,8 +143,10 @@
       mojom::FeaturePolicyFeature,
       ReportOptions report_on_failure = ReportOptions::kDoNotReport,
       const String& message = g_empty_string) const;
+  FeatureEnabledState GetFeatureEnabledState(mojom::FeaturePolicyFeature) const;
   virtual void ReportFeaturePolicyViolation(
       mojom::FeaturePolicyFeature,
+      mojom::FeaturePolicyDisposition,
       const String& message = g_empty_string) const {}
 
   // Apply the sandbox flag. In addition, if the origin is not already opaque,
@@ -165,6 +172,7 @@
   scoped_refptr<SecurityOrigin> security_origin_;
   Member<ContentSecurityPolicy> content_security_policy_;
   std::unique_ptr<FeaturePolicy> feature_policy_;
+  std::unique_ptr<FeaturePolicy> report_only_feature_policy_;
 
   mojom::IPAddressSpace address_space_;
   WebInsecureRequestPolicy insecure_request_policy_;
diff --git a/third_party/blink/renderer/core/feature_policy/feature_policy.cc b/third_party/blink/renderer/core/feature_policy/feature_policy.cc
index 5f6828e..63876c9 100644
--- a/third_party/blink/renderer/core/feature_policy/feature_policy.cc
+++ b/third_party/blink/renderer/core/feature_policy/feature_policy.cc
@@ -17,6 +17,9 @@
 
 namespace blink {
 
+constexpr char kReportOnlySuffix[] = "-report-only";
+constexpr size_t kReportOnlySuffixLength = 12;
+
 ParsedFeaturePolicy ParseFeaturePolicyHeader(
     const String& policy,
     scoped_refptr<const SecurityOrigin> origin,
@@ -64,15 +67,31 @@
       // Empty policy. Skip.
       if (tokens.IsEmpty())
         continue;
-      if (!feature_names.Contains(tokens[0])) {
-        if (messages)
+      mojom::FeaturePolicyDisposition disposition =
+          mojom::FeaturePolicyDisposition::kEnforce;
+      String feature_name;
+      if (RuntimeEnabledFeatures::FeaturePolicyReportingEnabled() &&
+          tokens[0].EndsWith(kReportOnlySuffix)) {
+        feature_name = tokens[0].Substring(
+            0, tokens[0].length() - kReportOnlySuffixLength);
+        disposition = mojom::FeaturePolicyDisposition::kReport;
+      } else {
+        feature_name = tokens[0];
+      }
+      if (!feature_names.Contains(feature_name)) {
+        if (messages) {
+          // Console message should display the entire string, with
+          // "-report-only" suffix if it was originally included.
           messages->push_back("Unrecognized feature: '" + tokens[0] + "'.");
+        }
         continue;
       }
 
-      mojom::FeaturePolicyFeature feature = feature_names.at(tokens[0]);
+      mojom::FeaturePolicyFeature feature = feature_names.at(feature_name);
       // If a policy has already been specified for the current feature, drop
       // the new policy.
+      // TODO(crbug.com/904880): Allow a report-only and an enforcing version in
+      // the same parsed policy.
       if (features_specified.QuickGet(static_cast<int>(feature)))
         continue;
 
@@ -92,6 +111,7 @@
 
       ParsedFeaturePolicyDeclaration allowlist;
       allowlist.feature = feature;
+      allowlist.disposition = disposition;
       features_specified.QuickSet(static_cast<int>(feature));
       std::vector<url::Origin> origins;
       // If a policy entry has no (optional) values (e,g,
@@ -181,6 +201,7 @@
   allowlist.feature = feature;
   allowlist.matches_all_origins = false;
   allowlist.matches_opaque_src = false;
+  allowlist.disposition = mojom::FeaturePolicyDisposition::kEnforce;
   policy.push_back(allowlist);
   return true;
 }
@@ -193,6 +214,7 @@
   allowlist.feature = feature;
   allowlist.matches_all_origins = true;
   allowlist.matches_opaque_src = true;
+  allowlist.disposition = mojom::FeaturePolicyDisposition::kEnforce;
   policy.push_back(allowlist);
   return true;
 }
diff --git a/third_party/blink/renderer/core/feature_policy/feature_policy_test.cc b/third_party/blink/renderer/core/feature_policy/feature_policy_test.cc
index 3ec5bf5..54d36ab 100644
--- a/third_party/blink/renderer/core/feature_policy/feature_policy_test.cc
+++ b/third_party/blink/renderer/core/feature_policy/feature_policy_test.cc
@@ -439,10 +439,12 @@
   ParsedFeaturePolicy test_policy = {{mojom::FeaturePolicyFeature::kFullscreen,
                                       false,
                                       false,
+                                      mojom::FeaturePolicyDisposition::kEnforce,
                                       {url_origin_a_, url_origin_b_}},
                                      {mojom::FeaturePolicyFeature::kGeolocation,
                                       false,
                                       false,
+                                      mojom::FeaturePolicyDisposition::kEnforce,
                                       {url_origin_a_}}};
   ParsedFeaturePolicy empty_policy = {};
 };
diff --git a/third_party/blink/renderer/core/feature_policy/iframe_policy.h b/third_party/blink/renderer/core/feature_policy/iframe_policy.h
index e53ec64..d4645da 100644
--- a/third_party/blink/renderer/core/feature_policy/iframe_policy.h
+++ b/third_party/blink/renderer/core/feature_policy/iframe_policy.h
@@ -33,7 +33,9 @@
       const ParsedFeaturePolicy& container_policy,
       scoped_refptr<const SecurityOrigin> src_origin) override {
     policy_ = FeaturePolicy::CreateFromParentPolicy(
-        parent_document_->GetFeaturePolicy(), container_policy,
+        parent_document_->GetFeaturePolicy(),
+        *DirectivesWithDisposition(mojom::FeaturePolicyDisposition::kEnforce,
+                                   container_policy),
         src_origin->ToUrlOrigin());
   }
 
diff --git a/third_party/blink/renderer/core/frame/feature_policy_violation_report_body.h b/third_party/blink/renderer/core/frame/feature_policy_violation_report_body.h
index b0a54f2e..ebe0794f 100644
--- a/third_party/blink/renderer/core/frame/feature_policy_violation_report_body.h
+++ b/third_party/blink/renderer/core/frame/feature_policy_violation_report_body.h
@@ -16,15 +16,20 @@
  public:
   FeaturePolicyViolationReportBody(const String& feature,
                                    const String& message,
+                                   const String& disposition,
                                    std::unique_ptr<SourceLocation> location)
-      : MessageReportBody(message, std::move(location)), feature_(feature) {}
+      : MessageReportBody(message, std::move(location)),
+        feature_(feature),
+        disposition_(disposition) {}
 
   String feature() const { return feature_; }
+  String disposition() const { return disposition_; }
 
   ~FeaturePolicyViolationReportBody() override = default;
 
  private:
   const String feature_;
+  const String disposition_;
 };
 
 }  // namespace blink
diff --git a/third_party/blink/renderer/core/frame/feature_policy_violation_report_body.idl b/third_party/blink/renderer/core/frame/feature_policy_violation_report_body.idl
index 42f3ef1c..968088b0 100644
--- a/third_party/blink/renderer/core/frame/feature_policy_violation_report_body.idl
+++ b/third_party/blink/renderer/core/frame/feature_policy_violation_report_body.idl
@@ -8,6 +8,7 @@
     NoInterfaceObject
 ] interface FeaturePolicyViolationReportBody : ReportBody {
   readonly attribute DOMString feature;
+  readonly attribute DOMString disposition;
   readonly attribute DOMString message;
   readonly attribute DOMString? sourceFile;
   readonly attribute unsigned long? lineNumber;
diff --git a/third_party/blink/renderer/core/html/html_frame_element.cc b/third_party/blink/renderer/core/html/html_frame_element.cc
index 46555b9..dc1bdd9 100644
--- a/third_party/blink/renderer/core/html/html_frame_element.cc
+++ b/third_party/blink/renderer/core/html/html_frame_element.cc
@@ -85,14 +85,15 @@
 ParsedFeaturePolicy HTMLFrameElement::ConstructContainerPolicy(
     Vector<String>*) const {
   // Frame elements are not allowed to enable the fullscreen feature. Add an
-  // empty whitelist for the fullscreen feature so that the framed content is
+  // empty allowlist for the fullscreen feature so that the framed content is
   // unable to use the API, regardless of origin.
   // https://fullscreen.spec.whatwg.org/#model
   ParsedFeaturePolicy container_policy;
-  ParsedFeaturePolicyDeclaration whitelist;
-  whitelist.feature = mojom::FeaturePolicyFeature::kFullscreen;
-  whitelist.matches_all_origins = false;
-  container_policy.push_back(whitelist);
+  ParsedFeaturePolicyDeclaration allowlist;
+  allowlist.feature = mojom::FeaturePolicyFeature::kFullscreen;
+  allowlist.matches_all_origins = false;
+  allowlist.disposition = mojom::FeaturePolicyDisposition::kEnforce;
+  container_policy.push_back(allowlist);
   return container_policy;
 }
 
diff --git a/third_party/blink/renderer/core/html/html_plugin_element.cc b/third_party/blink/renderer/core/html/html_plugin_element.cc
index f7d52096..dec73f7 100644
--- a/third_party/blink/renderer/core/html/html_plugin_element.cc
+++ b/third_party/blink/renderer/core/html/html_plugin_element.cc
@@ -299,15 +299,16 @@
 ParsedFeaturePolicy HTMLPlugInElement::ConstructContainerPolicy(
     Vector<String>*) const {
   // Plugin elements (<object> and <embed>) are not allowed to enable the
-  // fullscreen feature. Add an empty whitelist for the fullscreen feature so
+  // fullscreen feature. Add an empty allowlist for the fullscreen feature so
   // that the nested browsing context is unable to use the API, regardless of
   // origin.
   // https://fullscreen.spec.whatwg.org/#model
   ParsedFeaturePolicy container_policy;
-  ParsedFeaturePolicyDeclaration whitelist;
-  whitelist.feature = mojom::FeaturePolicyFeature::kFullscreen;
-  whitelist.matches_all_origins = false;
-  container_policy.push_back(whitelist);
+  ParsedFeaturePolicyDeclaration allowlist;
+  allowlist.feature = mojom::FeaturePolicyFeature::kFullscreen;
+  allowlist.matches_all_origins = false;
+  allowlist.disposition = mojom::FeaturePolicyDisposition::kEnforce;
+  container_policy.push_back(allowlist);
   return container_policy;
 }
 
diff --git a/third_party/blink/renderer/core/html/media/media_element_parser_helpers.cc b/third_party/blink/renderer/core/html/media/media_element_parser_helpers.cc
index 8ac695d..8ab20da 100644
--- a/third_party/blink/renderer/core/html/media/media_element_parser_helpers.cc
+++ b/third_party/blink/renderer/core/html/media/media_element_parser_helpers.cc
@@ -69,7 +69,8 @@
   if (!style.LogicalWidth().IsSpecified() &&
       !style.LogicalHeight().IsSpecified()) {
     layout_object->GetDocument().ReportFeaturePolicyViolation(
-        mojom::FeaturePolicyFeature::kUnsizedMedia);
+        mojom::FeaturePolicyFeature::kUnsizedMedia,
+        mojom::FeaturePolicyDisposition::kEnforce);
   }
 }
 
diff --git a/third_party/blink/renderer/modules/webusb/usb.cc b/third_party/blink/renderer/modules/webusb/usb.cc
index 647b920..d41521b2 100644
--- a/third_party/blink/renderer/modules/webusb/usb.cc
+++ b/third_party/blink/renderer/modules/webusb/usb.cc
@@ -110,11 +110,19 @@
         script_state,
         DOMException::Create(DOMExceptionCode::kNotSupportedError));
   }
-  if (!IsFeatureEnabled()) {
+
+  FeatureEnabledState state = GetFeatureEnabledState();
+  if (state != FeatureEnabledState::kEnabled) {
     ExecutionContext* execution_context = ExecutionContext::From(script_state);
     if (auto* document = DynamicTo<Document>(execution_context)) {
-      document->ReportFeaturePolicyViolation(mojom::FeaturePolicyFeature::kUsb);
+      document->ReportFeaturePolicyViolation(
+          mojom::FeaturePolicyFeature::kUsb,
+          (state == FeatureEnabledState::kReportOnly
+               ? mojom::FeaturePolicyDisposition::kReport
+               : mojom::FeaturePolicyDisposition::kEnforce));
     }
+  }
+  if (state == FeatureEnabledState::kDisabled) {
     return ScriptPromise::RejectWithDOMException(
         script_state, DOMException::Create(DOMExceptionCode::kSecurityError,
                                            kFeaturePolicyBlocked));
@@ -271,7 +279,8 @@
     return;
   }
 
-  if (!IsContextSupported() || !IsFeatureEnabled())
+  if (!IsContextSupported() ||
+      GetFeatureEnabledState() == FeatureEnabledState::kDisabled)
     return;
 
   EnsureServiceConnection();
@@ -282,7 +291,7 @@
     return;
 
   DCHECK(IsContextSupported());
-  DCHECK(IsFeatureEnabled());
+  DCHECK(GetFeatureEnabledState() != FeatureEnabledState::kDisabled);
   GetExecutionContext()->GetInterfaceProvider()->GetInterface(
       mojo::MakeRequest(&service_));
   service_.set_connection_error_handler(
@@ -311,8 +320,8 @@
   return true;
 }
 
-bool USB::IsFeatureEnabled() const {
-  return GetExecutionContext()->GetSecurityContext().IsFeatureEnabled(
+FeatureEnabledState USB::GetFeatureEnabledState() const {
+  return GetExecutionContext()->GetSecurityContext().GetFeatureEnabledState(
       mojom::FeaturePolicyFeature::kUsb);
 }
 
diff --git a/third_party/blink/renderer/modules/webusb/usb.h b/third_party/blink/renderer/modules/webusb/usb.h
index 8a37d0c..b19b102 100644
--- a/third_party/blink/renderer/modules/webusb/usb.h
+++ b/third_party/blink/renderer/modules/webusb/usb.h
@@ -12,6 +12,7 @@
 #include "third_party/blink/renderer/core/dom/context_lifecycle_observer.h"
 #include "third_party/blink/renderer/core/dom/events/event_target.h"
 #include "third_party/blink/renderer/core/execution_context/execution_context.h"
+#include "third_party/blink/renderer/core/execution_context/security_context.h"
 #include "third_party/blink/renderer/platform/bindings/script_wrappable.h"
 #include "third_party/blink/renderer/platform/heap/handle.h"
 
@@ -79,7 +80,7 @@
   void EnsureServiceConnection();
 
   bool IsContextSupported() const;
-  bool IsFeatureEnabled() const;
+  FeatureEnabledState GetFeatureEnabledState() const;
 
   mojom::blink::WebUsbServicePtr service_;
   HeapHashSet<Member<ScriptPromiseResolver>> get_devices_requests_;