You can’t fix what you can’t see. CSP reporting is how you see what’s happening with your Content Security Policy in production — without blocking anything.

Think of it as a canary in a coal mine. The canary doesn’t prevent problems. It tells you about them early enough to do something about it.

report-uri vs report-to

There are two ways to receive CSP violation reports. Confusingly, they coexist and serve slightly different purposes.

report-uri (Legacy but Reliable)

Content-Security-Policy: default-src 'self'; report-uri /api/csp-report

This has been around since CSP was first introduced. It’s supported by every browser that supports CSP. The browser sends a POST request to the specified URL with a JSON body containing the violation details.

report-to (Modern but Inconsistent)

Report-To: {
  "group": "csp-violations",
  "max_age": 31536000,
  "endpoints": [{"url": "https://yoursite.com/api/reports"}]
}

Content-Security-Policy: default-src 'self'; report-to csp-violations

This is the newer Reporting API. It’s designed to be more general (not just CSP — any kind of report). The problem is browser support is inconsistent, and the format is different from report-uri.

My recommendation: use both. report-uri for reliability, report-to for future-proofing. They can coexist in the same policy.

Setting Up a Report Endpoint

The violation report is a POST request with content type application/csp-report and a JSON body like this:

{
  "csp-report": {
    "document-uri": "https://yoursite.com/page",
    "referrer": "https://google.com/",
    "violated-directive": "script-src 'self'",
    "effective-directive": "script-src",
    "original-policy": "default-src 'self'; script-src 'self'",
    "disposition": "report",
    "blocked-uri": "inline",
    "line-number": 42,
    "source-file": "https://yoursite.com/page",
    "status-code": 200,
    "script-sample": ""
  }
}

Here’s how to handle this in different frameworks:

Express.js

const express = require('express');
const app = express();

// CSP reports use a specific content type
app.post('/api/csp-report', 
  express.json({ type: 'application/csp-report' }),
  (req, res) => {
    const report = req.body['csp-report'];
    
    console.error('[CSP Violation]', {
      page: report['document-uri'],
      directive: report['violated-directive'],
      blocked: report['blocked-uri'],
      source: report['source-file'],
      line: report['line-number'],
    });
    
    res.status(204).end();
  }
);
```text

### Laravel

Route::post(’/csp-report’, function (Request $request) { $report = json_decode($request->getContent(), true); $csp = $report[‘csp-report’] ?? [];

Log::channel('csp')->warning('CSP Violation', [
    'page' => $csp['document-uri'] ?? 'unknown',
    'directive' => $csp['violated-directive'] ?? 'unknown',
    'blocked' => $csp['blocked-uri'] ?? 'unknown',
    'source' => $csp['source-file'] ?? 'unknown',
    'line' => $csp['line-number'] ?? null,
]);

return response('', 204);

});



### Django

from django.views.decorators.csrf import csrf_exempt from django.http import JsonResponse import json import logging

logger = logging.getLogger(‘csp’)

@csrf_exempt def csp_report(request): if request.method == ‘POST’: report = json.loads(request.body) csp = report.get(‘csp-report’, {}) logger.warning(‘CSP Violation’, extra={ ‘page’: csp.get(‘document-uri’), ‘directive’: csp.get(‘violated-directive’), ‘blocked’: csp.get(‘blocked-uri’), }) return JsonResponse({}, status=204)


## Third-Party Report Services

If you don't want to build your own endpoint, there are hosted solutions:

### Report URI (report-uri.com)

The most popular option. Free tier gives you basic reporting for one domain. Paid plans add more domains and longer retention.

Setup is just adding their URL to your policy:

report-uri https://your-id.report-uri.com/r/d/csp/enforce


### Sentry

If you already use Sentry for error tracking, it can collect CSP reports. Reports show up alongside your JavaScript errors, which gives you useful context.

### Axiom / Logtail

Log management services that can accept CSP reports. Good if you're already sending logs there.

## Report-Only Mode: Your Best Friend

The `Content-Security-Policy-Report-Only` header is the same as the regular CSP header, except it doesn't block anything. It only reports.

Content-Security-Policy-Report-Only: default-src ‘self’; script-src ‘self’; report-uri /api/csp-report


Always start here. Always. I don't care if your policy looks perfect on paper. Real-world pages load things you forgot about. Report-only mode catches those things without breaking your site.

## Dedicating Reports

You'll get a LOT of reports. Most of them will be the same violation repeated thousands of times. Here's how to handle the volume:

1. **Deduplicate** — Group by page + directive + blocked-uri. You don't need to see the same violation 10,000 times.

2. **Prioritize** — Focus on script-src violations first (XSS risk), then connect-src (data exfiltration), then everything else.

3. **Set up alerts** — Get notified when NEW types of violations appear, not when volume increases. A sudden spike in existing violations usually means a bot scanner found your site, not a real attack.

4. **Set a retention period** — Keep reports for 30-90 days. Beyond that, the data is noise.

## The Deployment Flow That Works

Here's the approach I've seen work consistently:

1. **Week 1-2:** Deploy strict policy in report-only mode. Accept the chaos.
2. **Week 3:** Analyze reports. Categorize by severity and frequency.
3. **Week 4:** Fix code issues (move inline scripts, remove dead third-party integrations). Update policy to allow legitimate resources.
4. **Week 5:** Deploy updated policy in report-only mode again. Verify violations dropped.
5. **Week 6:** Deploy enforcement header alongside report-only. Keep both.
6. **Week 7+:** Monitor. Remove report-only only after enforcement is stable for a month.

## Check Your Configuration