reporting-api

April 10, 2026 ยท View on GitHub

npm license

Express.js middleware for the Reporting API. Automatically wires up report-to / report-uri on your existing policy headers and gives you a ready-made endpoint to collect violation, deprecation, crash, and network error reports.

Supported headers and report types

HeaderShorthand
Content-Security-PolicyCSP
Content-Security-Policy-Report-Only
Cross-Origin-Opener-PolicyCOOP
Cross-Origin-Opener-Policy-Report-Only
Cross-Origin-Embedder-PolicyCOEP
Cross-Origin-Embedder-Policy-Report-Only
Permissions-Policy
Permissions-Policy-Report-Only
NEL (Network Error Logging)NEL

Plus Deprecation, Intervention, and Crash reports.

Backwards-compatible with CSP Level 2 report-uri for browsers that don't yet support the Reporting API.

Install

npm install reporting-api

Peer dependencies: express, zod, debug.

Quick start

import express from 'express';
import { reportingEndpoint, setupReportingHeaders } from 'reporting-api';

const app = express();

// 1. Mount the reporting endpoint
app.use('/reporting-endpoint', reportingEndpoint({
  allowedOrigins: '*',
  onReport(report) {
    console.log(report.type, report.body);
  },
}));

// 2. Set your policy headers, then let the middleware attach reporters
app.use((req, res, next) => {
  res.setHeader('Content-Security-Policy', "script-src 'self'");
  res.setHeader('Cross-Origin-Opener-Policy', 'same-origin');
  res.setHeader('Cross-Origin-Embedder-Policy', 'require-corp');
  next();
});
app.use(setupReportingHeaders('/reporting-endpoint'));

app.listen(8080);

Note

Policy headers must be set before setupReportingHeaders runs so the middleware can append report-to and report-uri directives to them.

The resulting response headers will look like this:

Reporting-Endpoints: reporter="/reporting-endpoint"
Content-Security-Policy: script-src 'self';report-uri /reporting-endpoint?disposition=enforce;report-to reporter
Cross-Origin-Opener-Policy: same-origin;report-to="reporter"
Cross-Origin-Embedder-Policy: require-corp;report-to="reporter"

API

reportingEndpoint(config)

Returns Express middleware that accepts incoming reports.

OptionTypeDescription
onReport(report, req) => voidCalled for every valid report.
onValidationError(error, body, req) => voidCalled when a report fails Zod validation.
allowedOriginsstring | RegExp | ArrayEnable CORS for cross-origin reports. Use '*' to allow any origin.
ignoreBrowserExtensionsbooleanDrop CSP violations originating from browser extensions.
ignoredDeprecationIdsstring[]Deprecation report IDs to ignore (e.g. ['AttributionReporting', 'Topics']).
maxAgenumberMaximum report age in seconds. Older buffered reports are dropped.
debugbooleanEnable debug logging for the reporting-api:* namespace.

setupReportingHeaders(url, config?)

Returns Express middleware that appends report-to / report-uri to every policy header already set on the response and adds the Reporting-Endpoints header.

OptionTypeDefaultDescription
reportingGroupstring"reporter"Reporting group name.
enableDefaultReportersbooleanfalseUse the default group so you also receive Deprecation, Crash, and Intervention reports.
enableNetworkErrorLoggingboolean | objectfalseAdd Report-To + NEL headers (Reporting API v0, required for NEL). Accepts { success_fraction, failure_fraction, include_subdomains }.
versionstring | numberโ€”Appended as a ?version= query param so you can correlate reports with policy revisions.

Report schema

Every report delivered to onReport is validated with Zod and has the shape:

{
  type: 'csp-violation' | 'coop' | 'coep' | 'deprecation' | 'crash'
       | 'intervention' | 'network-error' | 'permissions-policy-violation'
       | 'potential-permissions-policy-violation';
  body: { /* type-specific fields */ };
  url: string;
  age: number;
  user_agent: string;
  report_format: 'report-uri' | 'report-to' | 'report-to-safari';
  version?: string;
}

Full type definitions are exported as Report and the individual body types (ContentSecurityPolicyReport, CrossOriginOpenerPolicyReport, etc.).

Client-side observing

Reports can also be observed in the browser via ReportingObserver:

if (typeof ReportingObserver !== 'undefined') {
  new ReportingObserver((reports) => {
    reports.forEach(r => console.log(r.body));
  }).observe();
}

Resources

Notes

  • Permissions-Policy reports to the default group when report-to is not set.
  • COOP and COEP require report-to values wrapped in double quotes (e.g. report-to="group").
  • Safari sends reports as { body: { ... } } instead of an array and omits age.