Monitor your web application with the Reporting API

Use the Reporting API to monitor security violations, deprecated API calls, and more.

Maud Nalpas
Maud Nalpas

Some errors only occur in production. You won't see them locally or during development because real users, real networks, and real devices change the game. The Reporting API helps catch some of these errors—such as security violations or deprecated and soon-to-be-deprecated API calls across your site, and transmits them to an endpoint you've specified.

It lets you declare what you'd like to monitor via HTTP headers, and is operated by the browser.

Setting up the Reporting API gives you peace of mind that when users experience these types of errors, you'll know, so you can fix them.

This post covers what this API can do and how to use it. Let's dive in!

Demo and code

See the Reporting API in action starting from Chrome 96 and newer (Chrome Beta or Canary, as of October 2021).

Overview

Diagram summarizing the steps below, from report generation to report access by the developer
How reports are generated and sent.

Let's assume that your site, site.example, has a Content-Security-Policy and a Document-Policy. Don't know what these do? That's okay, you'll still be able to understand this example.

You decide to monitor your site in order to know when these policies are violated, but also because you want to keep an eye on deprecated or soon-to-be-deprecated APIs your codebase may be using.

To do so, you configure a Reporting-Endpoints header, and map these endpoint names via the report-to directive in your policies where needed.

Reporting-Endpoints: main-endpoint="https://reports.example/main", default="https://reports.example/default"
# Content-Security-Policy violations and Document-Policy violations
# will be sent to main-endpoint
Content-Security-Policy: script-src 'self'; object-src 'none'; report-to main-endpoint;
Document-Policy: document-write=?0; report-to=main-endpoint;
# Deprecation reports don't need an explicit endpoint because
# these reports are always sent to the `default` endpoint

Something unforeseen happens, and these policies get violated for some of your users.

Example violations

index.html

<script src="script.js"></script>
<!-- CSP VIOLATION: Try to load a script that's forbidden as per the Content-Security-Policy -->
<script src="https://example.com/script.js"></script>

script.js, loaded by index.html

// DOCUMENT-POLICY VIOLATION: Attempt to use document.write despite the document policy
try {
  document.write('<h1>hi</h1>');
} catch (e) {
  console.log(e);
}
// DEPRECATION: Call a deprecated API
const webkitStorageInfo = window.webkitStorageInfo;

The browser generates a CSP violation report, a Document-Policy violation report, and a Deprecation report that capture these issues.

With a short delay—up to a minute—the browser then sends the reports to the endpoint that was configured for this violation type. The reports are sent out-of-band by the browser itself (not by your server nor by your site).

The endpoint(s) receive(s) these reports.

You can now access the reports on these endpoints and monitor what went wrong. You're ready to start troubleshooting the problem that's affecting your users.

Example report

{
  "age": 2,
  "body": {
    "blockedURL": "https://site2.example/script.js",
    "disposition": "enforce",
    "documentURL": "https://site.example",
    "effectiveDirective": "script-src-elem",
    "originalPolicy": "script-src 'self'; object-src 'none'; report-to main-endpoint;",
    "referrer": "https://site.example",
    "sample": "",
    "statusCode": 200
  },
  "type": "csp-violation",
  "url": "https://site.example",
  "user_agent": "Mozilla/5.0... Chrome/92.0.4504.0"
}

Use cases and report types

The Reporting API can be configured to help you monitor many types of interesting warnings or issues that happen throughout your site:

Report type Example of a situation where a report would be generated
CSP violation (Level 3 only) You've set a Content-Security-Policy (CSP) on one of your pages, but the page is trying to load a script that's not allowed by your CSP.
COOP violation You've set a Cross-Origin-Opener-Policy on a page, but a cross-origin window is trying to interact directly with the document.
COEP violation You've set a Cross-Origin-Embedder-Policy on a page, but the document includes a cross-origin iframe that has not opted into being loaded by cross-origin documents.
Document Policy violation The page has a document policy that prevents usage of document.write, but a script tries to call document.write.
Permissions policy violation The page has a permissions policy that prevents microphone usage, and a script that requests audio input.
Deprecation warning The page is using an API that is deprecated or will be deprecated; it calls it directly or via a top-level third-party script.
Intervention The page is trying to do something that the browser decides not to honor, for security, performance or user experience reasons. Example in Chrome: the page uses document.write on slow networks or calls navigator.vibrate in a cross-origin frame that the user hasn't interacted with yet.
Crash The browser crashes while your site is open.

Reports

What do reports look like?

The browser sends reports to the endpoint you've configured. It sends requests that look as follows:

POST
Content-Type: application/reports+json

The payload of these requests is a list of reports.

Example list of reports

[
  {
    "age": 420,
    "body": {
      "columnNumber": 12,
      "disposition": "enforce",
      "lineNumber": 11,
      "message": "Document policy violation: document-write is not allowed in this document.",
      "policyId": "document-write",
      "sourceFile": "https://site.example/script.js"
    },
    "type": "document-policy-violation",
    "url": "https://site.example/",
    "user_agent": "Mozilla/5.0... Chrome/92.0.4504.0"
  },
  {
    "age": 510,
    "body": {
      "blockedURL": "https://site.example/img.jpg",
      "destination": "image",
      "disposition": "enforce",
      "type": "corp"
    },
    "type": "coep",
    "url": "https://dummy.example/",
    "user_agent": "Mozilla/5.0... Chrome/92.0.4504.0"
  }
]

Here's the data you can find in each of these reports:

Field Description
age The number of milliseconds between the report's timestamp and the current time.
body The actual report data, serialized into a JSON string. The fields contained in a report's body are determined by the report's type. ⚠️ Reports of different types have different bodies. To see the exact body of each report type, check out the demo reporting endpoint and follow the instructions to generate example reports.
type A report type, for example csp-violation or coep.
url The address of the document or worker from which the report was generated. Sensitive data such as username, password, and fragment are stripped from this URL.
user_agent The User-Agent header of the request from which the report was generated.

Credentialed reports

Reporting endpoints that have the same origin as the page that generates the report receive the credentials (cookies) in the requests that contain the reports.

Credentials may give useful additional context about the report; for example, whether a given user’s account is triggering errors consistently, or if a certain sequence of actions taken on other pages is triggering a report on this page.

When and how does the browser send reports?

Reports are delivered out-of-band from your site: the browser controls when they're sent to the configured endpoint(s). There's also no way to control when the browser sends reports; it captures, queues, and sends them automatically at a suitable time.

This means that there's little to no performance concern when using the Reporting API.

Reports are sent with a delay—up to a minute—to increase the chances to send reports in batches. This saves bandwidth to be respectful to the user's network connection, which is especially important on mobile. The browser can also delay delivery if it's busy processing higher priority work, or if the user is on a slow and/or congested network at the time.

Third-party and first-party issues

Reports that are generated due to violations or deprecations happening on your page will be sent to the endpoint(s) you've configured. This includes violations committed by third-party scripts running on your page.

Violations or deprecations that happened in a cross-origin iframe embedded in your page will not be reported to your endpoint(s) (at least not by default). An iframe could set up its own reporting and even report to your site's—that is, the first-party's—reporting service; but that's up to the framed site. Also note that most reports are generated only if a page's policy is violated, and that your page's policies and the iframe's policies are different.

Example with deprecations

If the Reporting-Endpoints header is set up on your page: deprecated API called by third-party scripts running on your page will be reported to your endpoint. Deprecated API called by an iframe embedded in your page will not be reported to your endpoint. A deprecation report will be generated only if the iframe server has set up Reporting-Endpoints, and this report will be sent to whichever endpoint the iframe's server has set up.
If the Reporting-Endpoints header is set up on your page: deprecated API called by third-party scripts running on your page will be reported to your endpoint. Deprecated API called by an iframe embedded in your page will not be reported to your endpoint. A deprecation report will be generated only if the iframe server has set up Reporting-Endpoints, and this report will be sent to whichever endpoint the iframe's server has set up.

Browser support

The table below sums up browser support for the Reporting API v1, that is with the Reporting-Endpoints header. Browser support for the Reporting API v0 (Report-To header) is the same, except for one report type: Network Error Logging isn't supported in the new Reporting API. Read the migration guide for details.

Report type Chrome Chrome iOS Safari Firefox Edge
CSP violation (Level 3 only)* ✔ Yes ✔ Yes ✔ Yes ✘ No ✔ Yes
Network Error Logging ✘ No ✘ No ✘ No ✘ No ✘ No
COOP/COEP violation ✔ Yes ✘ No ✔ Yes ✘ No ✔ Yes
All other types: Document Policy violation, Deprecation, Intervention, Crash ✔ Yes ✘ No ✘ No ✘ No ✔ Yes

This table only summarizes support for report-to with the new Reporting-Endpoints header. Read the CSP reporting migration tips if you're looking to migrate to Reporting-Endpoints.

Using the Reporting API

Decide where reports should be sent

You have two options:

  • Send reports to an existing report collector service.
  • Send reports to a reporting collector you build and operate yourself.

Option 1: Use an existing report collector service

Some examples of report collector services are:

If you know of other solutions, open an issue to let us know, and we'll update this post!

Beside pricing, consider the following points when selecting a report collector: 🧐

  • Does this collector support all report types? For example, not all reporting endpoint solutions support COOP/COEP reports.
  • Are you comfortable sharing any of your application's URLs with a third-party report collector? Even if the browser strips sensitive information from these URLs, sensitive information may get leaked this way. If this sounds too risky for your application, operate your own reporting endpoint.

Option 2: Build and operate your own report collector

Building your own server that receives reports isn't that trivial. To get started, you can fork our lightweight boilerplate. It's built with Express and can receive and display reports.

  1. Head over to the boilerplate report collector.

  2. Click Remix to Edit to make the project editable.

  3. You now have your clone! You can customize it for your own purposes.

If you're not using the boilerplate and are building your own server from scratch:

  • Check for POST requests with a Content-Type of application/reports+json to recognize reports requests sent by the browser to your endpoint.
  • If your endpoint lives on a different origin than your site, ensure it supports CORS preflight requests.

Option 3: Combine Option 1 and 2

You may want to let a specific provider take care of some types of reports, but have an in-house solution for others.

In this case, set multiple endpoints as follows:

Reporting-Endpoints: endpoint-1="https://reports-collector.example", endpoint-2="https://my-custom-endpoint.example"

Configure the Reporting-Endpoints header

Set a Reporting-Endpoints response header. Its value must be one or a series of comma-separated key-value pairs:

Reporting-Endpoints: main-endpoint="https://reports.example/main", default="https://reports.example/default"

If you're migrating from the legacy Reporting API to the new Reporting API, it may make sense to set both Reporting-Endpoints and Report-To. See details in the migration guide. In particular, if you're using reporting for Content-Security-Policy violations via the report-uri directive only, check the migration steps for CSP reporting.

Reporting-Endpoints: main-endpoint="https://reports.example/main", default="https://reports.example/default"
Report-To: ...

Keys (endpoint names)

Each key can be a name of your choice, such as main-endpoint or endpoint-1. You can decide to set different named endpoints for different report types—for example, my-coop-endpoint, my-csp-endpoint. With this, you can route reports to different endpoints depending on their type.

If you want to receive intervention, deprecation and/or crash reports, set an endpoint named default.

If the Reporting-Endpoints header defines no default endpoint, reports of this type will not be sent (although they will be generated).

Values (URLs)

Each value is a URL of your choice, where the reports will be sent to. The URL to set here depends on what you decided in Step 1.

An endpoint URL:

Examples

Reporting-Endpoints: my-coop-endpoint="https://reports.example/coop", my-csp-endpoint="https://reports.example/csp", default="https://reports.example/default"

You can then use each named endpoint in the appropriate policy, or use one single endpoint across all policies.

Where to set the header?

In the new Reporting API—the one that is covered in this post— reports are scoped to documents. This means that for one given origin, different documents, such as site.example/page1 and site.example/page2, can send reports to different endpoints.

To receive report for violations or deprecations take place on any page of your site, set the header as a middleware on all responses.

Here's an example in Express:

const REPORTING_ENDPOINT_BASE = 'https://report.example';
const REPORTING_ENDPOINT_MAIN = `${REPORTING_ENDPOINT_BASE}/main`;
const REPORTING_ENDPOINT_DEFAULT = `${REPORTING_ENDPOINT_BASE}/default`;

app.use(function (request, response, next) {
  // Set up the Reporting API
  response.set(
    'Reporting-Endpoints',
    `main-endpoint="${REPORTING_ENDPOINT_MAIN}", default="${REPORTING_ENDPOINT_DEFAULT}"`,
  );
  next();
});

Edit your policies

Now that the Reporting-Endpoints header is configured, add a report-to directive to each policy header for which you wish to receive violation reports. The value of report-to should be one of the named endpoints you've configured.

You can use the multiple endpoint for multiple policies, or use different endpoints across policies.

For each policy, the value of report-to should be one of the named endpoints you've configured.

report-to is not needed for deprecation, intervention and crash reports. These reports aren't bound to any policy. They're generated as long as a default endpoint is set up and are sent to this default endpoint.

Example

# Content-Security-Policy violations and Document-Policy violations
# will be sent to main-endpoint
Content-Security-Policy: script-src 'self'; object-src 'none'; report-to main-endpoint;
Document-Policy: document-write=?0;report-to=main-endpoint;
# Deprecation reports don't need an explicit endpoint because
# these reports are always sent to the default endpoint

Example code

To see all this in context, below is an example Node server that uses Express and brings together all the pieces discussed in this article. It shows how to configure reporting for several different report types and displays the results.

Debug your reporting setup

Intentionally generate reports

When setting up the Reporting API, you'll likely need to intentionally violate your policies in order to check if reports are generated and sent as expected. To see example code that violates policies and does other bad things that will generate reports of all types, check out the demo.

Save time

Reports may be sent with a delay—about a minute, which is a long time when debugging. 😴 Luckily, when debugging in Chrome, you can use the flag --short-reporting-delay to receive reports as soon as they're generated.

Run this command in your terminal to turn on this flag:

YOUR_PATH/TO/EXECUTABLE/Chrome --short-reporting-delay

Use DevTools

In Chrome, use DevTools to see the reports that have been sent or will be sent.

As of October 2021, this feature is experimental. To use it, follow these steps:

  1. Use Chrome version 96 and newer (check by typing chrome://version in your browser)
  2. Type or paste chrome://flags/#enable-experimental-web-platform-features in Chrome's URL bar.
  3. Click Enabled.
  4. Restart your browser.
  5. Open Chrome DevTools.
  6. In Chrome DevTools, open the Settings. Under Experiments, click Enable Reporting API panel in the Application panel.
  7. Reload DevTools.
  8. Reload your page. Reports generated by the page DevTools is open in will be listed in Chrome DevTools' Application panel, under Reporting API.
Screenshot of DevTools listing the reports
Chrome DevTools displays the reports generated on your page and their status.

Report status

The Status column tells you if a report has been successfully sent.

Status Description
Success The browser has sent the report and the endpoint replied with a success code (200 or another success response code 2xx).
Pending The browser is currently making an attempt to send the report.
Queued The report has been generated and the browser is not currently trying to send it. A report appears as Queued in one of these two cases:
  • The report is new and the browser is waiting to see if more reports arrive before trying to send it.
  • The report is not new; the browser has already tried to send this report and has failed, and is waiting before trying again.
MarkedForRemoval After retrying for a while (Queued), the browser has stopped trying to send the report and will soon remove it from its list of reports to send.

Reports are removed after a while, whether or not they're successfully sent.

Troubleshooting

Are reports not generated or not sent as expected to your endpoint? Here are a few tips to troubleshoot this.

Reports are not generated

Reports that show up in DevTools have been correctly generated. If the report you expect does not show up in this list:

  • Check report-to in your policies. If this is misconfigured, a report won't be generated. Head over to Edit your policies to fix this. An additional way to troubleshoot this is to check the DevTools console in Chrome: if an error pops up in the console for the violation you expected, this means your policy is probably properly configured.
  • Keep in mind that only the reports that were generated for the document DevTools is open in will show up in this list. One example: if your site site1.example embeds an iframe site2.example that violates a policy and hence generates a report, this report will show up in DevTools only if you open the iframe in its own window and open DevTools for that window.

Reports are generated but not sent or not received

What if you can see a report in DevTools, but your endpoint doesn't receive it?

  • Make sure to use short delays. Maybe the reason you can't see a report is because it hasn't been sent yet!
  • Check your Reporting-Endpoints header configuration. If there's an issue with it, a report that has been generated correctly will not be sent. In DevTools, the report's status will remain Queued (it might jump to Pending, and then quickly back to Queued when a delivery attempt is made) in this case. Some common mistakes that may cause this:

  • The endpoint is used but not configured. Example:

Code with a mistake
 Document-Policy: document-write=?0;report-to=endpoint-1;
 Reporting-Endpoints: default="https://reports.example/default"

Document-Policy violation reports should be sent to endpoint-1, but this endpoint name isn't configured in Reporting-Endpoints.

  • The default endpoint is missing. Some reports types, such as deprecation and intervention reports, will only be sent to the endpoint named default. Read more in Configure the Reporting-Endpoints header.

  • Look for issues in your policy headers syntax, such as missing quotes. See details.

  • Check that your endpoint can handle incoming requests.

    • Make sure that your endpoint support CORS preflight requests. If it doesn't, it can't receive reports.

    • Test your endpoint's behavior. To do so, instead of generating reports manually, you can emulate the browser by sending to your endpoint requests that look like what the browser would send. Run the following:

    curl --header "Content-Type: application/reports+json" \
      --request POST \
      --data '[{"age":420,"body":{"columnNumber":12,"disposition":"enforce","lineNumber":11,"message":"Document policy violation: document-write is not allowed in this document.","policyId":"document-write","sourceFile":"https://dummy.example/script.js"},"type":"document-policy-violation","url":"https://dummy.example/","user_agent":"xxx"},{"age":510,"body":{"blockedURL":"https://dummy.example/img.jpg","destination":"image","disposition":"enforce","type":"corp"},"type":"coep","url":"https://dummy.example/","user_agent":"xxx"}]' \
      YOUR_ENDPOINT
    

    Your endpoint should respond with a success code (200 or another success response code 2xx). If it doesn't, there's an issue with its configuration.

Report-Only

-Report-Only policy headers and the Reporting-Endpoints work together.

Endpoints configured in Reporting-Endpoints and specified in the report-to field of Content-Security-Policy, Cross-Origin-Embedder-Policy and Cross-Origin-Opener-Policy, will receive reports when these policies are violated.

Endpoints configured in Reporting-Endpoints can also be specified in the report-to field of Content-Security-Policy-Report-Only, Cross-Origin-Embedder-Policy-Report-Only and Cross-Origin-Opener-Policy-Report-Only. They'll also receive reports when these policies are violated.

While reports are sent in both cases, -Report-Only headers do not enforce the policies: nothing will break or actually get blocked, but you will receive reports of what would have broken or been blocked.

ReportingObserver

The ReportingObserver JavaScript API can help you observe client-side warnings.

ReportingObserver and the Reporting-Endpoints header generate reports that look the same, but they enable slightly different uses cases.

Use ReportingObserver if:

  • You only want to monitor deprecations and/or browser interventions. ReportingObserver surfaces client-side warnings such as deprecations and browser interventions, but unlike Reporting-Endpoints, it doesn't capture any other types of reports such as CSP or COOP/COEP violations.
  • You need to react to these violations in real-time. ReportingObserver makes it possible to attach a callback to a violation event.
  • You want to attach additional information to a report to aid in debugging, via the custom callback.

Another difference is that ReportingObserver is configured only client-side: you can use it even if you have no control over server-side headers and can't set Reporting-Endpoints.

Further reading

Hero image by Nine Koepfer / @enka80 on Unsplash, edited. Many thanks to Ian Clelland, Eiji Kitamura and Milica Mihajlija for their reviews and suggestions on this article.