Your Analytics Script Is Blocked by Content Security Policy: How to Diagnose and Fix It
EngineeringJuly 19, 202611 min read

Your Analytics Script Is Blocked by Content Security Policy: How to Diagnose and Fix It

CSP silently drops your analytics. No errors in your app, no data in your dashboard. Here's how to read the violation and fix it in under 10 minutes.

The dashboard showed zero pageviews. Not low pageviews — zero. For three days.

I checked the script tag. Present. I checked the site ID. Correct. I checked the network tab. Nothing. No requests to our analytics endpoint at all. The script wasn't failing with an error. It wasn't throwing an exception. It simply... wasn't running.

Took me forty minutes to think of opening the console. One line, buried under twelve React hydration warnings:

Refused to load the script 'https://cdn.justanalytics.app/script.js' because it violates the following Content Security Policy directive: "script-src 'self'".

Ah. CSP.

The deploy two weeks earlier had added a Content Security Policy header. Security team request. Nobody thought to check if it would break analytics. Why would they? The site rendered perfectly. No visible errors. Just a silent policy violation that dropped our tracking script on the floor.

Three days of visitor data, gone. And I only caught it because someone asked why the traffic report was blank.

Why CSP Blocks Are So Easy to Miss

Content Security Policy is a browser security feature that restricts which resources a page can load. Designed to prevent XSS attacks by whitelisting trusted sources for scripts, styles, images, and network requests.

The problem: CSP fails silently. (Much like how ad blockers silently break tracking — different cause, same symptom.)

If a resource violates the policy, the browser refuses to load it — but continues rendering everything else. Your app works. Your users see nothing wrong. The blocked script just never executes. No error boundary to catch. No exception to log. No 500 page. Just... missing data.

And you won't notice until someone pulls a report and asks why traffic dropped 30% overnight. I've been that someone, staring at a blank report, wondering if Google shut off the internet.

I've seen teams blame ad blockers, blame the analytics vendor, blame "a bug in the tracking code" — all before anyone thought to check CSP headers. (I've been on those teams. I've blamed those things.) It's not the first thing you look for. Maybe it should be.

Reading the CSP Violation: What the Console Actually Tells You

When CSP blocks a resource, the browser logs a violation to the console. Here's how to read it.

Script blocked from loading:

Refused to load the script 'https://cdn.example.com/script.js' because it violates
the following Content Security Policy directive: "script-src 'self'".

This means your script-src directive doesn't include the script's domain. The script never loads. No tracking happens.

Beacon or fetch blocked from sending:

Refused to connect to 'https://api.example.com/event' because it violates
the following Content Security Policy directive: "connect-src 'self'".

This means your connect-src directive doesn't include the API endpoint. The script loads and runs, but every network request it makes gets dropped. Events fire locally but never reach the server.

Inline script blocked:

Refused to execute inline script because it violates the following
Content Security Policy directive: "script-src 'self'". Either the 'unsafe-inline'
keyword, a hash ('sha256-abc123...'), or a nonce ('nonce-xyz789') is required.

This means you have an inline <script> tag and your policy doesn't allow it. Common with analytics snippets that embed config directly.

The violation message tells you exactly what's wrong. The directive name (script-src, connect-src, style-src) tells you which part of your policy to fix. The quoted value ('self', 'none', a domain) tells you what the current whitelist is.

If you're not seeing console warnings, CSP might not be the issue. Check your network tab for failed requests with other status codes.

The Two Directives That Matter for Analytics

Analytics tools need two CSP directives configured correctly:

script-src — Controls which scripts can load.

Your analytics script lives on a CDN (like cdn.justanalytics.app or www.googletagmanager.com). If that domain isn't in your script-src, the browser won't load it.

connect-src — Controls which endpoints scripts can call.

After loading, the script sends events to a collection endpoint (like api.justanalytics.app or www.google-analytics.com). If that domain isn't in your connect-src, every beacon request fails silently.

Missing script-src → script doesn't load at all. Missing connect-src → script loads, but events vanish into the void.

Both symptoms look the same in your dashboard: zero data. But the console violations differ, and the fix differs.

Here's a minimal CSP that allows JustAnalytics:

Content-Security-Policy:
  default-src 'self';
  script-src 'self' https://cdn.justanalytics.app;
  connect-src 'self' https://api.justanalytics.app;

For GA4, you'd need:

Content-Security-Policy:
  default-src 'self';
  script-src 'self' https://www.googletagmanager.com https://www.google-analytics.com;
  connect-src 'self' https://www.google-analytics.com https://analytics.google.com;

The exact domains depend on your vendor. Check their documentation — most have a CSP compatibility page buried somewhere. (And I mean buried. Three levels deep, under "Advanced Configuration," behind a link labeled "Security." Every time.)

Step-by-Step: Diagnosing Your CSP Block

Here's the debugging flow I use now. Takes about five minutes once you know what to look for.

Step 1: Check if CSP is even active

Open DevTools → Network tab → reload the page → click on the main document request → look at Response Headers.

Search for Content-Security-Policy or Content-Security-Policy-Report-Only. If neither exists, CSP isn't your problem — go debug something else.

If you see a CSP header, copy the whole thing. You'll need to read it.

Step 2: Find your analytics script in the policy

Your CSP header looks something like this mess:

default-src 'self'; script-src 'self' 'unsafe-inline' https://trusted-cdn.com;
style-src 'self' 'unsafe-inline'; img-src 'self' data: https:;
connect-src 'self' https://api.trusted-cdn.com; font-src 'self';

Find the script-src directive. Is your analytics CDN domain listed? If you're using JustAnalytics, look for cdn.justanalytics.app. If it's not there, that's your problem.

Find the connect-src directive. Is your analytics API domain listed? For JustAnalytics, look for api.justanalytics.app. If connect-src doesn't exist at all, it inherits from default-src — which is usually 'self', which won't include external APIs.

Step 3: Confirm with the console

Open DevTools → Console tab → reload the page → filter for "CSP" or "Content Security Policy".

You should see the exact violation. If you don't, either CSP isn't the issue, or your browser hides CSP warnings (some do by default — check your console filter settings).

Step 4: Test a fix locally

Before deploying to production, test your modified CSP. Most frameworks let you override headers locally.

In Next.js (next.config.js) — if you're running a Next.js observability stack, this is where you'll configure it:

module.exports = {
  async headers() {
    return [
      {
        source: '/(.*)',
        headers: [
          {
            key: 'Content-Security-Policy',
            value: "default-src 'self'; script-src 'self' https://cdn.justanalytics.app; connect-src 'self' https://api.justanalytics.app;"
          },
        ],
      },
    ];
  },
};

Reload, check the console, verify no violations. Check the network tab, verify the analytics script loads and events fire.

Step 5: Deploy and verify

Push the fix. Wait for the CDN to propagate (Vercel is instant; Cloudflare takes 30 seconds; some enterprise CDNs take minutes). Check your analytics dashboard — events should start appearing within a minute or two.

If you lost data during the outage, it's gone. CSP violations don't queue events for retry. That data is just... not coming back. Another reason to catch these fast. (We wrote about correlating errors with analytics funnel drop-off — that pattern would've flagged this incident via the sudden pageview cliff.)

The Proxy Workaround: When You Can't Modify the CSP

Sometimes you don't control the CSP. Maybe it's set by a security team with a change-management process. Maybe it's enforced by enterprise policy. Maybe your client's IT department locked it down.

The workaround: proxy the analytics script and endpoint through your own domain.

If your CSP allows 'self', you can serve the analytics script from /ja/script.js and route events through /ja/event. To the browser, these are same-origin requests. CSP allows them.

In Next.js:

// next.config.js
module.exports = {
  async rewrites() {
    return [
      {
        source: '/ja/script.js',
        destination: 'https://cdn.justanalytics.app/script.js'
      },
      {
        source: '/ja/event',
        destination: 'https://api.justanalytics.app/event'
      },
    ];
  },
};

Update your script tag:

<script
  defer
  data-site="your-site-id"
  data-api="/ja/event"
  src="/ja/script.js">
</script>

This works for nginx too:

location /ja/script.js {
    proxy_pass https://cdn.justanalytics.app/script.js;
}

location /ja/event {
    proxy_pass https://api.justanalytics.app/event;
}

The proxy adds a small latency overhead (one extra hop), but it's usually under 20ms. Worth it to get your tracking working. Honestly, I wish more docs covered this option upfront — it's saved me multiple times when dealing with enterprise IT policies that move slower than continental drift.

Bonus: this also bypasses ad blockers that filter by domain. We've seen 10-15% tracking lifts from proxying on developer-focused sites. Same technique, different problem.

Common Mistakes That Break Analytics After a CSP Fix

You fixed the CSP. Analytics still doesn't work. Here's what I've seen go wrong:

Typo in the domain. cdn.justanalytic.app vs cdn.justanalytics.app. One missing 's'. CSP is exact-match. Triple-check your domains. (Ask me how I know.)

HTTP vs HTTPS. http://cdn.example.com and https://cdn.example.com are different origins. If your CSP specifies https: and the script tag uses HTTP, blocked. If you just write the domain without protocol (cdn.example.com), CSP defaults to matching the page's protocol.

Missing wildcard for subdomains. *.example.com covers cdn.example.com and api.example.com. Just example.com covers only the root. Some vendors use multiple subdomains — and they don't always document which ones.

Forgetting connect-src. You added the script domain to script-src. Script loads. But events don't send because connect-src still blocks the API endpoint. I've done this at least three times. The fourth time I'll probably do it again.

Cache not cleared. Your old CSP header is cached at the CDN or in the browser. Hard refresh (Cmd+Shift+R) or wait for cache TTL. Cloudflare caches headers aggressively — purge the cache if you're impatient. Or drink coffee and wait. (For teams managing multiple web properties, DevOS can automate cache purging across CDNs.)

Report-only mode confusion. Content-Security-Policy-Report-Only doesn't enforce — it just reports violations. If you're testing with report-only and then switch to enforcing, behavior changes. Make sure you're testing the actual enforcing header.

The Five-Minute Checklist

If your analytics script stopped working and you suspect CSP:

  1. Open DevTools Console. Look for "Content Security Policy" or "Refused to load/connect". No warnings? Probably not CSP.

  2. Read the violation. script-src issue = script doesn't load. connect-src issue = events don't send.

  3. Find your CSP header. Network tab → main document → Response Headers. Copy it.

  4. Add the missing domain. Analytics CDN to script-src. Analytics API to connect-src.

  5. Deploy and verify. Check console for no violations. Check network tab for successful requests. Check dashboard for incoming events.

If you can't modify the CSP, proxy the script and endpoint through your own domain using rewrites. We covered more advanced proxy configurations for enterprise environments if you're dealing with complex setups.

For teams also running paid acquisition, CSP blocking your ad-fraud protection is equally dangerous. ClickzProtect needs its script and endpoint whitelisted the same way — if CSP blocks click-fraud detection, you're bleeding money while thinking you're protected.

Frequently Asked Questions

Why does CSP block analytics scripts silently without breaking my site?

CSP is designed to fail closed — if a resource violates the policy, the browser refuses to load it but continues rendering the rest of the page. Your app works fine, your users see nothing wrong, but the blocked script never executes. The only evidence is a console warning that most developers never see. This is why analytics gaps from CSP are so insidious: there's no visible error, just missing data.

What's the difference between script-src and connect-src in CSP?

script-src controls which JavaScript files can load and execute. connect-src controls which URLs your scripts can make network requests to — fetch, XMLHttpRequest, WebSocket, and beacon calls. Analytics needs both: script-src to load the tracking script, connect-src to send events to the collection endpoint. Missing either one blocks your analytics, but with different symptoms.

Can I use a nonce instead of whitelisting the analytics domain?

Yes, if your server generates HTML dynamically. Add a cryptographic nonce to your CSP header and the script tag: script-src 'nonce-abc123' and <script nonce='abc123'>. The nonce must be unique per request. This works well for SSR frameworks like Next.js but not for static sites where the HTML is pre-built. For static sites, domain whitelisting is the only option.

Why do I see CSP violations for report-uri or report-to endpoints?

CSP violation reports are POST requests sent by the browser to your reporting endpoint. If your CSP blocks the reporting endpoint itself via connect-src, the reports can't be sent — which is ironic. Make sure your report-uri or report-to domain is also listed in connect-src. Some teams accidentally block their own monitoring by forgetting this.


Try JustAnalytics

All-in-one observability in one under-5KB script: cookieless analytics + error tracking + APM + session replay + uptime + structured logs. Replaces GA4 + Sentry + Datadog + Pingdom + LogRocket. Free tier (100K events/mo), Pro at $49/month ($39/month if you pay annually).

Start free → · AI Command Center MCP

JP
JustAnalytics Platform TeamContributor

Author at JustAnalytics.

Related posts