The Reporting API
TL;DR
The Reporting API defines a new HTTP header, Report-To
, that gives
web developers a way to specify server endpoints for the browser
to send warnings and errors to. Browser-generated warnings like
CSP violations, Feature Policy violations, deprecations,
browser interventions, and network errors are
some of the things that can be collected using the
Reporting API.
Introduction
Some errors only occur in production (aka The Wild). You never see them locally or during development because real users, real networks, and real devices change the game. Not to mention all the cross browser issues that get thrown into the mix.
As an example, say your new site relies on document.write()
to load
critical scripts. New users from different parts of the world will eventually
find your site, but they're probably on much slower connections
than you tested with. Unbeknownst to you, your site starts breaking
for them because of Chrome's browser intervention
for blocking document.write() on 2G networks.
Yikes! Without the Reporting API there's no way to know
this is happening to your precious users.
The Reporting API helps catch potential (even future) errors across your site. Setting it up gives you "peace of mind" that nothing terrible is happening when real users visit your site. If/when they _do_ experience unforeseen errors, you'll be in the know. 👍
The Report-To Header
Right now, the API needs to be enabled using a
runtime command line flag: --enable-features=Reporting
.
The Reporting API introduces a new HTTP response header, Report-To
. Its
value is an object which describes an endpoint group for the browser
to report errors to:
Report-To: {
"max_age": 10886400,
"endpoints": [{
"url": "https://analytics.provider.com/browser-errors"
}]
}
Note: If your endpoint URL lives on a different origin than your site, the
endpoint should support CORs preflight requests. (e.g. Access-Control-Allow-Origin: *;
Access-Control-Allow-Methods: GET,PUT,POST,DELETE,OPTIONS; Access-Control-Allow-Headers: Content-Type, Authorization, Content-Length, X-Requested-With
In the example, sending this response header with your main page
configures the browser to report browser-generated warnings
to the endpoint https://example.com/browser-errors
for max_age
seconds.
It's important to note that all subsequent HTTP requests made by the page
(for images, scripts, etc.) are ignored. Configuration is setup during
the response of the main page.
Configuring multiple endpoints
A single response can configure several endpoints at once by sending
multiple Report-To
headers:
Report-To: {
"group": "default",
"max_age": 10886400,
"endpoints": [{
"url": "https://example.com/browser-reports"
}]
}
Report-To: {
"group": "csp-endpoint",
"max_age": 10886400,
"endpoints": [{
"url": "https://example.com/csp-reports"
}]
}
or by combining them into a single HTTP header:
Report-To: {
"group": "csp-endpoint",
"max_age": 10886400,
"endpoints": [{
"url": "https://example.com/csp-reports"
}]
},
{
"group": "network-endpoint",
"max_age": 10886400,
"endpoints": [{
"url": "https://example.com/network-errors"
}]
},
{
"max_age": 10886400,
"endpoints": [{
"url": "https://example.com/browser-errors"
}]
}
Once you've sent the Report-To
header, the browser caches the endpoints
according to their max_age
values, and sends all of those nasty console
warnings/errors to your URLs. Boom!
Explanation of header fields
Each endpoint configuration contains a group
name, max_age
, and endpoints
array. You can also choose whether to consider subdomains when reporting
errors by using the include_subdomains
field.
Field | Type | Description |
---|---|---|
group |
string | Optional. If a group name is not specified, the endpoint is given a name of "default". |
max_age |
number | Required. A non-negative integer that defines the lifetime of the endpoint in seconds. A value of "0" will cause the endpoint group to be removed from the user agent’s reporting cache. |
endpoints |
Array<Object> | Required. An array of JSON objects that specify the actual URL of your report collector. |
include_subdomains |
boolean | Optional. A boolean that enables the endpoint group for all subdomains of the current origin's host. If omitted or anything other than "true", the subdomains are not reported to the endpoint. |
The group
name is a unique name used to associate a string with
an endpoint. Use this name in other places that integrate
with the Reporting API to refer to a specific endpoint group.
The max-age
field is also required and specifies how
long the browser should use the endpoint and report errors to it.
The endpoints
field is an array to provide failover and load balancing
features. See the section on Failover and load balancing. It's
important to note that the browser will select only one endpoint, even
if the group lists several collectors in endpoints
. If you want to send a
report to several servers at once, your backend will need to forward the
reports.
How the browser sends a report
Reports are delivered out-of-band from your app, meaning the browser controls when reports are sent to your server(s). The browser attempts to deliver queued reports as soon as they're ready (in order to provide timely feedback to the developer) but it can also delay delivery if it's busy processing higher priority work or the user is on a slow and/or congested network at the time. The browser may also prioritize sending reports about a particular origin first, if the user is a frequent visitor.
The browser periodically batches reports and sends them to the reporting
endpoints that you configure. To send reports, the browser issues a POST
request with
Content-Type: application/reports+json
and a body containing the array of
warnings/errors which were captured.
Example - browser issues a POST
request to your CSP errors endpoint:
POST /csp-reports HTTP/1.1
Host: example.com
Content-Type: application/reports+json
[{
"type": "csp",
"age": 10,
"url": "https://example.com/vulnerable-page/",
"user_agent": "Mozilla/5.0 (X11; Linux x86_64; rv:60.0) Gecko/20100101 Firefox/60.0",
"body": {
"blocked": "https://evil.com/evil.js",
"directive": "script-src",
"policy": "script-src 'self'; object-src 'none'",
"status": 200,
"referrer": "https://evil.com/"
}
}, }
...
}]
Report types discusses how to set up CSP reporting.
The Reporting API was designed to be out of band from your web app. The browser captures, queues and batches, then sends reports automatically at the most opportune time. Reports are sent internally by the browser, so there's little to no performance concern (e.g. network contention with your app) when using the Reporting API. There's also no way to control when the browser sends queued reports.
Debugging report configurations
If you don't see reports showing up on your server, head over to
chrome://net-internals/#reporting
. That page is useful for
verifying things are configured correctly and reports are being sent
out properly.
Report types
The Reporting API can be used for more than just browser
intervention
and deprecation
messages. In fact, it can be configured to
send many other types of interesting warnings/issues that happen throughout
your site:
- CSP violations
- Deprecations
- Browser interventions
- Feature Policy violations (in development)
- Network Error Logging (NEL)
- Crash reports
CSP reports
A long time ago, the web elders realized that sending client-side CSP violations to a backend would be pretty handy. If your site breaks because of some new powerful feature (i.e. CSP), you probably want to be notified! Thus, we've had a reporting mechanism for CSP from day one.
When a site violates a CSP rule, it can (optionally) tell the browser
to send the error to a server. This is done by adding the
report-uri
directive
in the CSP header:
Content-Security-Policy: ...; report-uri https://example.com/csp-reports
Content-Security-Policy-Report-Only: ...; report-uri https://example.com/csp-reports
The Reporting API integrates with CSP reports by adding a new
report-to
directive.
Unlike report-uri
which takes a URL, report-to
takes an
endpoint group name. It still has a URL, but that gets moved inside endpoints
in the configuration object:
New
Content-Security-Policy-Report-Only: ...; report-to csp-endpoint
Report-To: {
...
}, {
"group": "csp-endpoint",
"max_age": 10886400,
"endpoints": [{
"url": "https://example.com/csp-reports"
}]
}
For backwards compatibility, continue to use report-uri
along with report-to
.
In other words: Content-Security-Policy: ...; report-uri https://endpoint.com; report-to groupname
.
Browsers that support report-to
will use it instead of the former.
Network errors
The Network Error Logging (NEL) spec defines a mechanism for
collecting client-side network errors from an origin. It uses the new NEL
HTTP
response header to setup to tell the browser collect network errors,
then integrates with the Reporting API to report the errors to a server.
To use NEL, first setup the Report-To
header with a
collector that uses a named group:
Report-To: {
...
}, {
"group": "network-errors",
"max_age": 2592000,
"endpoints": [{
"url": "https://analytics.provider.com/networkerrors"
}]
}
Next, send the NEL
response header to start collecting errors. Since NEL
is opt-in for an origin*, you only need to send the header once. Both NEL
and
Report-To
will apply to future requests to the same origin and will continue
to collect errors according to the max_age
value that was used to set up
the collector.
The header value should be a JSON object that contains a max_age
and
report_to
field. Use the latter to reference the group name of your
network errors collector:
GET /index.html HTTP/1.1
NEL: {"report_to": "network-errors", "max_age": 2592000}
The Report-To
header uses a hyphen. Here, report_to
uses an underscore.
Sub-resources
NEL works across navigations and subresources fetches. But for subresources,
there's an important point to highlight: the containing page has no visibility
into the NEL reports about cross-origin requests that it makes. This means
that if example.com
loads foobar.com/cat.gif
and that resource fails
to load, foobar.com
's NEL collector is notified, not example.com
's. The
rule of thumb is that NEL is reproducing server-side logs, just generated on
the client. Since example.com
has no visibility into foobar.com
's server
logs, it also has no visibility into its NEL reports.
Feature Policy violations
Currently, Feature Policy violations are not captured with the Reporting API. However, the plan is to integrate Feature Policy into the Reporting API.
Crash reports
Browser crash reports are also still in development but will eventually be capturable via the Reporting API.
Failover and load balancing
Most of the time you'll be configuring one URL collector per group. However, since reporting can generate a good deal of traffic, the spec includes failover and load-balancing features inspired by the DNS SRV record.
The browser will do its best to deliver a report to at most one endpoint
in a group. Endpoints can be assigned a weight
to distribute load, with each
endpoint receiving a specified fraction of reporting traffic. Endpoints can
also be assigned a priority
to set up fallback collectors.
Example - creating a fallback collector at https://backup.com/reports.
Report-To: {
"group": "endpoint-1",
"max_age": 10886400,
"endpoints": [
{"url": "https://example.com/reports", "priority": 1},
{"url": "https://backup.com/reports", "priority": 2}
]
}
Fallback collectors are only tried when uploads to primary collectors fail.
Example server
HTTP examples are great. Actual code is even better.
To see all this stuff 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 create separate handlers to capture the results.
const express = require('express');
const app = express();
app.use(express.json({
type: ['application/json', 'application/csp-report', 'application/reports+json']
}));
app.use(express.urlencoded());
app.get('/', (request, response) => {
// Note: report-to replaces report-uri, but it is not supported yet.
response.set('Content-Security-Policy-Report-Only',
`default-src 'self'; report-to csp-endpoint`);
// Note: report_to and not report-to for NEL.
response.set('NEL', `{"report_to": "network-errors", "max_age": 2592000}`);
// The Report-To header tells the browser where to send
// CSP violations, browser interventions, deprecations, and network errors.
// The default group (first example below) captures interventions and
// deprecation reports. Other groups are referenced by their "group" name.
response.set('Report-To', `{
"max_age": 2592000,
"endpoints": [{
"url": "https://reporting-observer-api-demo.glitch.me/reports"
}],
}, {
"group": "csp-endpoint",
"max_age": 2592000,
"endpoints": [{
"url": "https://reporting-observer-api-demo.glitch.me/csp-reports"
}],
}, {
"group": "network-errors",
"max_age": 2592000,
"endpoints": [{
"url": "https://reporting-observer-api-demo.glitch.me/network-reports"
}]
}`);
response.sendFile('./index.html');
});
function echoReports(request, response) {
// Record report in server logs or otherwise process results.
for (const report of request.body) {
console.log(report.body);
}
response.send(request.body);
}
app.post('/csp-reports', (request, response) => {
console.log(`${request.body.length} CSP violation reports:`);
echoReports(request, response);
});
app.post('/network-reports', (request, response) => {
console.log(`${request.body.length} Network error reports:`);
echoReports(request, response);
});
app.post('/reports', (request, response) => {
console.log(`${request.body.length} deprecation/intervention reports:`);
echoReports(request, response);
});
const listener = app.listen(process.env.PORT, () => {
console.log(`Your app is listening on port ${listener.address().port}`);
});
What about ReportingObserver?
Although both are part of the same Reporting API spec,
ReportingObserver and the Report-To
header have overlap
with each other but enable slightly different uses cases.
ReportingObserver
is a JavaScript API that can observe simple
client-side warnings like deprecation and intervention. Reports
are not automatically sent to a server (unless you choose to do so in the callback):
const observer = new ReportingObserver((reports, observer) => {
for (const report of reports) {
// Send report somewhere.
}
}, {buffered: true});
observer.observe();
More sensitive types of errors like CSP violations and network errors
cannot be observed by a ReportingObserver
. Enter Report-To
.
The Report-To
header is more powerful in that it can capture
more types of error reports (network, CSP, browser crashes)
in addition to the ones supported in ReportingObserver
. Use it when you
want to automatically report errors to a server or capture errors
that are otherwise impossible to see in JavaScript (network errors).
Conclusion
Although the Reporting API is a ways out from shipping in all browsers, it's a promising tool for diagnosing issues across your site.
Warnings that get logged to the DevTools console are super helpful but have limited value to you as the site author. That's because they're local to the user's browser! The Reporting API changes this. Use it to configure, detect, and report to a server even errors even when your own code cannot. Propagate browser warnings to a backend, catch issues across your site before they grow out of control, and prevent future bugs before they happen (e.g. know about deprecated APIs ahead of their removal).