skip to content

You ship a Content-Security-Policy-Report-Only header and the collector immediately fills with thousands of violations you cannot reproduce locally. How do you separate real breakage from noise before switching the policy to enforcing?

level: seniorimportance: should knowfreq 36%

answer

  1. most of it is not your code
  2. look at the scheme first
  3. cluster, do not enumerate
  4. inline reports carry no URL
  5. report-only sees only walked paths

basics

~20 s

Most of that volume comes from browser extensions and injecting middleboxes, not your code. Triage by grouping on effectiveDirective plus sourceFile, discarding non-http schemes such as extension URLs, and listening for securitypolicyviolation in the page to attach route and release context.

solid answer

~50 s

Expect the majority of the volume to be noise. Extensions inject scripts and styles into every page, and their violations surface with `blockedURI` or `sourceFile` values on schemes like `chrome-extension:` and `moz-extension:`; captive portals and carrier proxies rewrite HTML and add their own. Neither is your app breaking. So triage in layers: drop reports whose source is not an http(s) URL of yours, then group what remains by `effectiveDirective` plus `sourceFile` — a handful of clusters usually accounts for nearly everything. Reports are also deliberately coarse: inline and eval violations report `blockedURI` as `"inline"` or `"eval"`, and cross-origin blocks are stripped to the origin, so you need your own context. Adding a `securitypolicyviolation` listener on `document` lets you tag each violation with the route, release and whether the disposition was report-only, and rate-limit before sending. Finally, remember report-only only covers paths real traffic reaches, so walk the rare flows by hand before enforcing.

code

javascript · 22 lines
javascript
const seen = new Set();

document.addEventListener('securitypolicyviolation', (event) => {
  // Extension and middlebox noise: not our code
  const source = event.sourceFile || '';
  if (source && !source.startsWith('http')) return;

  // Collapse to one report per cluster per page view
  const key = `${event.effectiveDirective}|${source}|${event.lineNumber}`;
  if (seen.has(key)) return;
  seen.add(key);

  navigator.sendBeacon('/csp-triage', JSON.stringify({
    directive: event.effectiveDirective,
    blockedURI: event.blockedURI,
    sourceFile: source,
    lineNumber: event.lineNumber,
    sample: event.sample,
    disposition: event.disposition,
    path: location.pathname
  }));
});

go deeper

for a junior

Know that report-only mode observes without blocking, and that a large share of the reports it produces come from browser extensions rather than from your code.

for a middle

Be able to walk the fields of a violation — effectiveDirective, blockedURI, sourceFile, disposition — and explain why inline and cross-origin violations are reported coarsely.

for a senior

Show a triage method under real volume: filter by scheme and host, cluster by directive and source, rank by affected sessions, and collect in-page context with the securitypolicyviolation event before enforcing.

for a principal

Own the rollout as a programme: what evidence justifies promoting a candidate policy, how the collector's cost and retention are bounded, and who is accountable when a third-party tag forces the policy to loosen.

## Why the flood happens The first day of report-only is the same for everyone: a volume of reports one or two orders of magnitude above what your app could possibly be generating, most of it irreproducible. Understanding the sources is the whole skill. **Browser extensions.** Extensions inject content scripts and stylesheets into pages, and those injections are subject to your policy in ways that generate violations. They appear with `blockedURI` or `sourceFile` values on extension schemes — `chrome-extension:`, `moz-extension:`, `safari-web-extension:` — or as inline violations originating from an extension frame. Ad blockers, password managers, translators, accessibility tools, coupon finders and corporate agents are all in this set. You cannot fix them, and enforcing will not break the site for those users in the way the report implies. **Injecting middleboxes.** Some mobile carriers, captive portals, hotel networks, antivirus products and corporate proxies still rewrite HTML in flight, adding scripts. Those produce violations from hosts you have never heard of. Serving over HTTPS removes most of this class, and its residue is a signal worth knowing about rather than a bug in your app. **Your actual breakage.** Usually a small number of distinct causes: an inline handler in a legacy template, a third-party tag loading a fourth-party script, a style element from a CSS-in-JS runtime, an eval inside one dependency. ## What a violation report actually carries Whether you read it from a report body or from the DOM event, the fields are the same: - `documentURI` — the page the violation happened on - `blockedURI` — what was refused - `effectiveDirective` / `violatedDirective` — which rule fired - `sourceFile`, `lineNumber`, `columnNumber` — where the offending code lives - `sample` — a short truncated excerpt for inline and eval violations, present only when the policy asks for it - `disposition` — `"enforce"` or `"report"` - `originalPolicy` — the full policy string, useful when several policies are live at once Two deliberate coarsenings matter for triage. Inline and eval violations do not report a URL — `blockedURI` is the literal string `"inline"` or `"eval"`, so every inline violation on the site collapses into one bucket that only `sourceFile` and `lineNumber` can separate. And cross-origin blocked URLs are stripped down to their origin, so you learn that something from `https://tag.example` was refused but not which path. Both exist to stop the reporting channel leaking information about the user's browsing, and both mean the report alone is often insufficient. ## A triage order that works 1. **Filter by scheme.** Discard anything whose `sourceFile` or `blockedURI` scheme is not `http:`/`https:`. That single rule typically removes most of the volume. 2. **Filter by host.** Keep hosts you recognise — your origins and your known vendors. Park the rest as "injected content", and check the list occasionally rather than per-report. 3. **Group, do not enumerate.** Aggregate on `effectiveDirective` + `sourceFile` (+ `lineNumber` for inline). Thousands of reports collapse into a handful of clusters, each of which is one code change. 4. **Rank by uniqueness of affected sessions, not by report count.** One user with a chatty extension and a long session can outweigh a real defect hitting a thousand people once each. 5. **Reproduce in a clean profile.** If a cluster does not reproduce with extensions disabled, it is almost certainly not yours. ## Collect in the page as well The report body carries no application context — no route name, no release, no feature flags. Listening in the page fixes that: ```js document.addEventListener('securitypolicyviolation', (e) => { send({ directive: e.effectiveDirective, blocked: e.blockedURI, source: e.sourceFile, line: e.lineNumber, sample: e.sample, disposition: e.disposition, // "report" or "enforce" route: currentRoute(), release: BUILD_ID }); }); ``` This also lets you sample and rate-limit before anything leaves the browser, which matters because the raw volume can be genuinely expensive. Note the event fires in the document where the violation occurred, so violations inside a frame need their own listener. ## The limits of report-only Two blind spots to close before you flip the switch: - **Coverage.** Report-only only tells you about paths real traffic exercises. Admin screens, error states, checkout edge cases and rarely used exports may never appear. Walk them deliberately with the report-only policy active. - **It is not a dry run of every consequence.** Because nothing is blocked, you never observe how the app behaves *after* a resource fails — whether a missing chunk degrades gracefully or leaves a blank screen. The practical endgame is to run both headers at once for a period: a loose enforcing policy that protects what you already trust, plus a strict report-only policy that measures your candidate. When the candidate's clusters are down to known noise, promote it.

  • Why does a blocked inline script report blockedURI as "inline" rather than a location?
    Because there is no URL to report — the code was in the document. The literal token `"inline"` marks the category, and `sourceFile` with `lineNumber` is the only positional information you get. Any excerpt in `sample` is short and truncated on purpose, since violation reports travel to a collector and must not leak page content.
  • A cluster of violations names a host you do not recognise, on pages across the whole site. How do you decide whether to act?
    Reproduce in a clean browser profile with extensions disabled and on a different network. If it disappears, it is injected by an extension or a middlebox rather than by your app, and no policy change of yours removes it. Check whether the affected sessions cluster on particular user agents or carriers, which usually confirms it outright.
  • Can you run an enforcing policy and a report-only policy at the same time?
    Yes, and it is the standard rollout: send `Content-Security-Policy` with the policy you already trust and `Content-Security-Policy-Report-Only` with the stricter candidate. Both are evaluated, only the first blocks, and each violation's `disposition` field says which one fired — so you measure the candidate under real traffic without risking the site.

saying these in an interview costs you the question

  • Treats every reported violation as application breakage
  • Enforces as soon as the report volume drops
  • Chases individual reports instead of grouping them
  • Expects a URL in blockedURI for inline violations
  • Assumes report-only exercises every route in the app

context