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)

C o n t e n t - S e c u r i t y - P o l i c y : d e f a u l t - s r c ' s e l f ' ; r e p o r t - u r i / a p i / c s p - r e p o r t

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)

R } C e o p " " " n o g m e t r r a n e t o x d n - u _ p t T p a o - o " g i S : : e n e " t c { " : s u c " r s 3 : i p 1 t - 5 [ y v 3 { - i 6 " P o 0 u o l 0 r l a 0 l i t , " c i : y o : n " s h d " t e , t f p a s u : l / t / - y s o r u c r s ' i s t e e l . f c ' o ; m / r a e p p i o / r r t e - p t o o r t c s s " p } - ] v i o l a t i o n s

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();
  }
);

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:

r e p o r t - u r i h t t p s : / / y o u r - i d . r e p o r t - u r i . c o m / r / d / c s p / e n f o r c e

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.

C o n d s r t e c e e f r p n a i o t u p r - l t t S t - - e - s u c s r r u r c i r c i ' / t ' s a y s e p - e l i P l f o f ' c l ' ; s i ; p c - y r - e R p e o p r o t r t - O n l y :

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