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/feature-policy/reporting/camera-report-only.https.html b/feature-policy/reporting/camera-report-only.https.html
new file mode 100644
index 0000000..2648868
--- /dev/null
+++ b/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/feature-policy/reporting/camera-report-only.https.html.headers b/feature-policy/reporting/camera-report-only.https.html.headers
new file mode 100644
index 0000000..46b8481
--- /dev/null
+++ b/feature-policy/reporting/camera-report-only.https.html.headers
@@ -0,0 +1 @@
+Feature-Policy: camera-report-only 'none'
diff --git a/feature-policy/reporting/camera-reporting.https.html b/feature-policy/reporting/camera-reporting.https.html
index 12b48dc..ced5851 100644
--- a/feature-policy/reporting/camera-reporting.https.html
+++ b/feature-policy/reporting/camera-reporting.https.html
@@ -13,6 +13,7 @@
   assert_equals(report.type, "feature-policy-violation");
   assert_equals(report.url, document.location.href);
   assert_equals(report.body.featureId, "camera");
+  assert_equals(report.body.disposition, "enforce");
   assert_equals(report.body.sourceFile, document.location.href);
   assert_equals(typeof report.body.lineNumber, "number");
   assert_equals(typeof report.body.columnNumber, "number");
diff --git a/feature-policy/reporting/document-write-report-only.html b/feature-policy/reporting/document-write-report-only.html
new file mode 100644
index 0000000..ab0bb82
--- /dev/null
+++ b/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/feature-policy/reporting/document-write-report-only.html.headers b/feature-policy/reporting/document-write-report-only.html.headers
new file mode 100644
index 0000000..63e43f1
--- /dev/null
+++ b/feature-policy/reporting/document-write-report-only.html.headers
@@ -0,0 +1 @@
+Feature-Policy: document-write-report-only 'none'
diff --git a/feature-policy/reporting/document-write-reporting.html b/feature-policy/reporting/document-write-reporting.html
index e67bff0..93672fa 100644
--- a/feature-policy/reporting/document-write-reporting.html
+++ b/feature-policy/reporting/document-write-reporting.html
@@ -13,6 +13,7 @@
   assert_equals(report.type, "feature-policy-violation");
   assert_equals(report.url, document.location.href);
   assert_equals(report.body.featureId, "document-write");
+  assert_equals(report.body.disposition, "enforce");
   assert_equals(report.body.sourceFile, document.location.href);
   assert_equals(typeof report.body.lineNumber, "number");
   assert_equals(typeof report.body.columnNumber, "number");
diff --git a/feature-policy/reporting/encrypted-media-report-only.https.html b/feature-policy/reporting/encrypted-media-report-only.https.html
new file mode 100644
index 0000000..20e44b2
--- /dev/null
+++ b/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/feature-policy/reporting/encrypted-media-report-only.https.html.headers b/feature-policy/reporting/encrypted-media-report-only.https.html.headers
new file mode 100644
index 0000000..a2c8fbb
--- /dev/null
+++ b/feature-policy/reporting/encrypted-media-report-only.https.html.headers
@@ -0,0 +1 @@
+Feature-Policy: encrypted-media-report-only 'none'
diff --git a/feature-policy/reporting/encrypted-media-reporting.https.html b/feature-policy/reporting/encrypted-media-reporting.https.html
index d309d53..c3b6393 100644
--- a/feature-policy/reporting/encrypted-media-reporting.https.html
+++ b/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/feature-policy/reporting/fullscreen-report-only.html b/feature-policy/reporting/fullscreen-report-only.html
new file mode 100644
index 0000000..a6b3d5a
--- /dev/null
+++ b/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/feature-policy/reporting/fullscreen-report-only.html.headers b/feature-policy/reporting/fullscreen-report-only.html.headers
new file mode 100644
index 0000000..33defa8
--- /dev/null
+++ b/feature-policy/reporting/fullscreen-report-only.html.headers
@@ -0,0 +1 @@
+Feature-Policy: fullscreen-report-only 'none'
diff --git a/feature-policy/reporting/fullscreen-reporting.html b/feature-policy/reporting/fullscreen-reporting.html
index 0153647..4190fdd 100644
--- a/feature-policy/reporting/fullscreen-reporting.html
+++ b/feature-policy/reporting/fullscreen-reporting.html
@@ -12,6 +12,7 @@
   assert_equals(report.type, "feature-policy-violation");
   assert_equals(report.url, document.location.href);
   assert_equals(report.body.featureId, "fullscreen");
+  assert_equals(report.body.disposition, "enforce");
   assert_equals(report.body.sourceFile, document.location.href);
   assert_equals(typeof report.body.lineNumber, "number");
   assert_equals(typeof report.body.columnNumber, "number");
diff --git a/feature-policy/reporting/generic-sensor-report-only.https.html b/feature-policy/reporting/generic-sensor-report-only.https.html
new file mode 100644
index 0000000..deb6ade
--- /dev/null
+++ b/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/feature-policy/reporting/generic-sensor-report-only.https.html.headers b/feature-policy/reporting/generic-sensor-report-only.https.html.headers
new file mode 100644
index 0000000..26605eb
--- /dev/null
+++ b/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/feature-policy/reporting/generic-sensor-reporting.https.html b/feature-policy/reporting/generic-sensor-reporting.https.html
index c60e3e8..517c7f6 100644
--- a/feature-policy/reporting/generic-sensor-reporting.https.html
+++ b/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/feature-policy/reporting/geolocation-report-only.https.html b/feature-policy/reporting/geolocation-report-only.https.html
new file mode 100644
index 0000000..cf2a75b
--- /dev/null
+++ b/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/feature-policy/reporting/geolocation-report-only.https.html.headers b/feature-policy/reporting/geolocation-report-only.https.html.headers
new file mode 100644
index 0000000..fc98590
--- /dev/null
+++ b/feature-policy/reporting/geolocation-report-only.https.html.headers
@@ -0,0 +1 @@
+Feature-Policy: geolocation-report-only 'none'
diff --git a/feature-policy/reporting/geolocation-reporting.https.html b/feature-policy/reporting/geolocation-reporting.https.html
index af4fd39..05445fc 100644
--- a/feature-policy/reporting/geolocation-reporting.https.html
+++ b/feature-policy/reporting/geolocation-reporting.https.html
@@ -13,6 +13,9 @@
   assert_equals(report.type, "feature-policy-violation");
   assert_equals(report.url, document.location.href);
   assert_equals(report.body.featureId, "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");
   assert_equals(typeof report.body.columnNumber, "number");
   assert_equals(report.body.disposition, "enforce");
diff --git a/feature-policy/reporting/microphone-report-only.https.html b/feature-policy/reporting/microphone-report-only.https.html
new file mode 100644
index 0000000..2d7b4d9
--- /dev/null
+++ b/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/feature-policy/reporting/microphone-report-only.https.html.headers b/feature-policy/reporting/microphone-report-only.https.html.headers
new file mode 100644
index 0000000..7673d05
--- /dev/null
+++ b/feature-policy/reporting/microphone-report-only.https.html.headers
@@ -0,0 +1 @@
+Feature-Policy: microphone-report-only 'none'
diff --git a/feature-policy/reporting/microphone-reporting.https.html b/feature-policy/reporting/microphone-reporting.https.html
index 2aeba81..246484f 100644
--- a/feature-policy/reporting/microphone-reporting.https.html
+++ b/feature-policy/reporting/microphone-reporting.https.html
@@ -13,6 +13,7 @@
   assert_equals(report.type, "feature-policy-violation");
   assert_equals(report.url, document.location.href);
   assert_equals(report.body.featureId, "microphone");
+  assert_equals(report.body.disposition, "enforce");
   assert_equals(report.body.sourceFile, document.location.href);
   assert_equals(typeof report.body.lineNumber, "number");
   assert_equals(typeof report.body.columnNumber, "number");
diff --git a/feature-policy/reporting/midi-report-only.html b/feature-policy/reporting/midi-report-only.html
new file mode 100644
index 0000000..e466ce0
--- /dev/null
+++ b/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/feature-policy/reporting/midi-report-only.html.headers b/feature-policy/reporting/midi-report-only.html.headers
new file mode 100644
index 0000000..3c6a2d4
--- /dev/null
+++ b/feature-policy/reporting/midi-report-only.html.headers
@@ -0,0 +1 @@
+Feature-Policy: midi-report-only 'none'
diff --git a/feature-policy/reporting/midi-reporting.html b/feature-policy/reporting/midi-reporting.html
index 94674a3..c5627e5 100644
--- a/feature-policy/reporting/midi-reporting.html
+++ b/feature-policy/reporting/midi-reporting.html
@@ -11,6 +11,7 @@
   assert_equals(report.type, "feature-policy-violation");
   assert_equals(report.url, document.location.href);
   assert_equals(report.body.featureId, "midi");
+  assert_equals(report.body.disposition, "enforce");
   assert_equals(report.body.sourceFile, document.location.href);
   assert_equals(typeof report.body.lineNumber, "number");
   assert_equals(typeof report.body.columnNumber, "number");
diff --git a/feature-policy/reporting/payment-report-only.https.html b/feature-policy/reporting/payment-report-only.https.html
new file mode 100644
index 0000000..6a7678b
--- /dev/null
+++ b/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/feature-policy/reporting/payment-report-only.https.html.headers b/feature-policy/reporting/payment-report-only.https.html.headers
new file mode 100644
index 0000000..6411478
--- /dev/null
+++ b/feature-policy/reporting/payment-report-only.https.html.headers
@@ -0,0 +1 @@
+Feature-Policy: payment-report-only 'none'
diff --git a/feature-policy/reporting/payment-reporting.https.html b/feature-policy/reporting/payment-reporting.https.html
index 6655210..828054a 100644
--- a/feature-policy/reporting/payment-reporting.https.html
+++ b/feature-policy/reporting/payment-reporting.https.html
@@ -13,6 +13,7 @@
   assert_equals(report.type, "feature-policy-violation");
   assert_equals(report.url, document.location.href);
   assert_equals(report.body.featureId, "payment");
+  assert_equals(report.body.disposition, "enforce");
   assert_equals(report.body.sourceFile, document.location.href);
   assert_equals(typeof report.body.lineNumber, "number");
   assert_equals(typeof report.body.columnNumber, "number");
diff --git a/feature-policy/reporting/picture-in-picture-report-only.html b/feature-policy/reporting/picture-in-picture-report-only.html
new file mode 100644
index 0000000..157670f
--- /dev/null
+++ b/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/feature-policy/reporting/picture-in-picture-report-only.html.headers b/feature-policy/reporting/picture-in-picture-report-only.html.headers
new file mode 100644
index 0000000..0df90a3
--- /dev/null
+++ b/feature-policy/reporting/picture-in-picture-report-only.html.headers
@@ -0,0 +1 @@
+Feature-Policy: picture-in-picture-report-only 'none'
diff --git a/feature-policy/reporting/picture-in-picture-reporting.html b/feature-policy/reporting/picture-in-picture-reporting.html
index e3cbf10..f15f47c 100644
--- a/feature-policy/reporting/picture-in-picture-reporting.html
+++ b/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/feature-policy/reporting/sync-xhr-report-only.html b/feature-policy/reporting/sync-xhr-report-only.html
new file mode 100644
index 0000000..f841f63
--- /dev/null
+++ b/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/feature-policy/reporting/sync-xhr-report-only.html.headers b/feature-policy/reporting/sync-xhr-report-only.html.headers
new file mode 100644
index 0000000..79a82cf
--- /dev/null
+++ b/feature-policy/reporting/sync-xhr-report-only.html.headers
@@ -0,0 +1 @@
+Feature-Policy: sync-xhr-report-only 'none'
diff --git a/feature-policy/reporting/sync-xhr-reporting.html b/feature-policy/reporting/sync-xhr-reporting.html
index d92a685..82200cd 100644
--- a/feature-policy/reporting/sync-xhr-reporting.html
+++ b/feature-policy/reporting/sync-xhr-reporting.html
@@ -13,6 +13,7 @@
   assert_equals(report.type, "feature-policy-violation");
   assert_equals(report.url, document.location.href);
   assert_equals(report.body.featureId, "sync-xhr");
+  assert_equals(report.body.disposition, "enforce");
   assert_equals(report.body.sourceFile, document.location.href);
   assert_equals(typeof report.body.lineNumber, "number");
   assert_equals(typeof report.body.columnNumber, "number");
diff --git a/feature-policy/reporting/usb-report-only.https.html b/feature-policy/reporting/usb-report-only.https.html
new file mode 100644
index 0000000..e44c6c5
--- /dev/null
+++ b/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/feature-policy/reporting/usb-report-only.https.html.headers b/feature-policy/reporting/usb-report-only.https.html.headers
new file mode 100644
index 0000000..bd2e3c6
--- /dev/null
+++ b/feature-policy/reporting/usb-report-only.https.html.headers
@@ -0,0 +1 @@
+Feature-Policy: usb-report-only 'none'
diff --git a/feature-policy/reporting/usb-reporting.https.html b/feature-policy/reporting/usb-reporting.https.html
index a63f301..0ddff45 100644
--- a/feature-policy/reporting/usb-reporting.https.html
+++ b/feature-policy/reporting/usb-reporting.https.html
@@ -13,6 +13,7 @@
   assert_equals(report.type, "feature-policy-violation");
   assert_equals(report.url, document.location.href);
   assert_equals(report.body.featureId, "usb");
+  assert_equals(report.body.disposition, "enforce");
   assert_equals(report.body.sourceFile, document.location.href);
   assert_equals(typeof report.body.lineNumber, "number");
   assert_equals(typeof report.body.columnNumber, "number");
diff --git a/feature-policy/reporting/vr-report-only.https.html b/feature-policy/reporting/vr-report-only.https.html
new file mode 100644
index 0000000..91016d3
--- /dev/null
+++ b/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/feature-policy/reporting/vr-report-only.https.html.headers b/feature-policy/reporting/vr-report-only.https.html.headers
new file mode 100644
index 0000000..b54cad2
--- /dev/null
+++ b/feature-policy/reporting/vr-report-only.https.html.headers
@@ -0,0 +1 @@
+Feature-Policy: vr-report-only 'none'
diff --git a/feature-policy/reporting/vr-reporting.https.html b/feature-policy/reporting/vr-reporting.https.html
index 42a2e73..e0bb635 100644
--- a/feature-policy/reporting/vr-reporting.https.html
+++ b/feature-policy/reporting/vr-reporting.https.html
@@ -11,6 +11,7 @@
   assert_equals(report.type, "feature-policy-violation");
   assert_equals(report.url, document.location.href);
   assert_equals(report.body.featureId, "vr");
+  assert_equals(report.body.disposition, "enforce");
   assert_equals(report.body.sourceFile, document.location.href);
   assert_equals(typeof report.body.lineNumber, "number");
   assert_equals(typeof report.body.columnNumber, "number");
diff --git a/feature-policy/reporting/xr-report-only.https.html b/feature-policy/reporting/xr-report-only.https.html
new file mode 100644
index 0000000..5d4fb06
--- /dev/null
+++ b/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/feature-policy/reporting/xr-report-only.https.html.headers b/feature-policy/reporting/xr-report-only.https.html.headers
new file mode 100644
index 0000000..b54cad2
--- /dev/null
+++ b/feature-policy/reporting/xr-report-only.https.html.headers
@@ -0,0 +1 @@
+Feature-Policy: vr-report-only 'none'
diff --git a/feature-policy/reporting/xr-reporting.https.html b/feature-policy/reporting/xr-reporting.https.html
index b737bb9..0844860 100644
--- a/feature-policy/reporting/xr-reporting.https.html
+++ b/feature-policy/reporting/xr-reporting.https.html
@@ -11,6 +11,7 @@
   assert_equals(report.type, "feature-policy-violation");
   assert_equals(report.url, document.location.href);
   assert_equals(report.body.featureId, "vr");
+  assert_equals(report.body.disposition, "enforce");
   assert_equals(report.body.sourceFile, document.location.href);
   assert_equals(typeof report.body.lineNumber, "number");
   assert_equals(typeof report.body.columnNumber, "number");