Sign up

Troubleshooting

Script blocked by Content Security Policy (CSP)

On this page

A Content Security Policy (CSP) tells the browser which scripts may run and where a page may send requests. If it doesn't list MetricRaven, the browser silently blocks the tracker and no visits arrive.

Check whether CSP is the problem

  1. Open your live site with DevTools

    Open the page in Chrome, Edge or Firefox and open DevTools (F12, or Cmd+Option+I on a Mac).

  2. Look in the Console

    A CSP block reads like this:

    Console
    Refused to load the script 'https://metricraven.com/js/script.js' because it violates the following
    Content Security Policy directive: "script-src 'self'".
    
    Refused to connect to 'https://metricraven.com/api/v1/collect' because it violates the following
    Content Security Policy directive: "connect-src 'self'".

    The first line means script-src blocks the script. The second means connect-src blocks the requests that send visits.

  3. Find the policy

    In the Network tab, select the page document and look for a Content-Security-Policy response header, or search the page source for http-equiv="Content-Security-Policy".

Allow MetricRaven

Add https://metricraven.com to two directives: script-src (loads /js/script.js) and connect-src (sends visits and events to /api/v1/collect). Keep everything else in your policy as it is:

The two directives to change
script-src 'self' https://metricraven.com; connect-src 'self' https://metricraven.com;
If your policy has no script-src or connect-src, the browser uses default-src instead. Add both directives rather than loosening default-src.

Next.js and Vercel

Static headers in next.config

next.config.ts
// next.config.ts
import type { NextConfig } from "next";

const csp = [
  "default-src 'self'",
  "script-src 'self' 'unsafe-inline' https://metricraven.com",
  "connect-src 'self' https://metricraven.com",
  "img-src 'self' data: blob:",
  "style-src 'self' 'unsafe-inline'",
].join("; ");

const nextConfig: NextConfig = {
  async headers() {
    return [{ source: "/(.*)", headers: [{ key: "Content-Security-Policy", value: csp }] }];
  },
};

export default nextConfig;

vercel.json

For any framework deployed on Vercel, set the header in vercel.json:

vercel.json
{
  "headers": [
    {
      "source": "/(.*)",
      "headers": [
        {
          "key": "Content-Security-Policy",
          "value": "script-src 'self' https://metricraven.com; connect-src 'self' https://metricraven.com;"
        }
      ]
    }
  ]
}

Nonce-based policies (strict-dynamic)

With 'strict-dynamic', browsers ignore host allowlists in script-src, so adding the domain isn't enough: the script tag needs the request's nonce. connect-src still needs https://metricraven.com.

proxy.ts
// proxy.ts: a fresh nonce per request ('strict-dynamic' ignores host allowlists)
import { NextResponse, type NextRequest } from "next/server";

export function proxy(request: NextRequest) {
  const nonce = Buffer.from(crypto.randomUUID()).toString("base64");
  const csp = [
    "default-src 'self'",
    `script-src 'self' 'nonce-${nonce}' 'strict-dynamic'`,
    "connect-src 'self' https://metricraven.com",
  ].join("; ");
  const headers = new Headers(request.headers);
  headers.set("x-nonce", nonce);
  headers.set("Content-Security-Policy", csp);
  const response = NextResponse.next({ request: { headers } });
  response.headers.set("Content-Security-Policy", csp);
  return response;
}
app/layout.tsx
// app/layout.tsx
import Script from "next/script";
import { headers } from "next/headers";

export default async function RootLayout({ children }: { children: React.ReactNode }) {
  const nonce = (await headers()).get("x-nonce") ?? undefined;
  return (
    <html lang="en">
      <head>
        <Script
          src="https://metricraven.com/js/script.js"
          data-site-id="YOUR_SITE_ID"
          strategy="afterInteractive"
          nonce={nonce}
        />
      </head>
      <body>{children}</body>
    </html>
  );
}

Outside Next.js, print the same per-request nonce on the script tag:

index.html
<script
  defer
  nonce="RANDOM_PER_REQUEST_VALUE"
  src="https://metricraven.com/js/script.js"
  data-site-id="YOUR_SITE_ID"
></script>

The npm package takes a nonce option for the script tag it injects. A nonce must be random and new on every response; a fixed value defeats the policy.

Netlify

_headers
# public/_headers (or the publish directory)
/*
  Content-Security-Policy: script-src 'self' https://metricraven.com; connect-src 'self' https://metricraven.com;
netlify.toml
# netlify.toml
[[headers]]
  for = "/*"
  [headers.values]
    Content-Security-Policy = "script-src 'self' https://metricraven.com; connect-src 'self' https://metricraven.com;"

HTML meta tag

If you can't set response headers, put the policy in a meta tag at the top of <head>:

index.html
<meta
  http-equiv="Content-Security-Policy"
  content="script-src 'self' https://metricraven.com; connect-src 'self' https://metricraven.com;"
/>

A meta policy applies only to elements after it, and it is combined with any header policy: if the server also sends a CSP header, that header must allow MetricRaven too.

Nginx

nginx.conf
# server { … } block, then: sudo nginx -t && sudo systemctl reload nginx
add_header Content-Security-Policy "script-src 'self' https://metricraven.com; connect-src 'self' https://metricraven.com;" always;

add_header in a location block replaces the ones from the server block, so add it wherever your other headers live.

Apache

.htaccess
# .htaccess or the VirtualHost (needs mod_headers)
<IfModule mod_headers.c>
  Header set Content-Security-Policy "script-src 'self' https://metricraven.com; connect-src 'self' https://metricraven.com;"
</IfModule>

WordPress

Security plugins (Wordfence, Sucuri, WP Cerber, Really Simple Security, Headers Security Advanced & HSTS) and some hosts set the policy for you. Find their CSP or HTTP headers setting and add https://metricraven.com to the script and connect sources. If your theme sets the header itself:

functions.php
// functions.php (child theme) or a small must-use plugin
add_action('send_headers', function () {
    header("Content-Security-Policy: script-src 'self' https://metricraven.com; connect-src 'self' https://metricraven.com;");
});

Serving tracking from your own domain

With a proxy, the script and its requests use your own domain, so 'self' already covers them and you don't need to list MetricRaven:

terminal
script-src 'self'; connect-src 'self';

If the proxy lives on another subdomain (for example stats.example.com), list that origin in both directives instead.

Verify it works

Reload the page with the cache disabled. The Console shows no CSP errors, the Network tab shows script.js and a /api/v1/collect request with ok: true, and the visit appears in your site's Traffic tab.

Still blocked?

SymptomCheck
The header you changed isn't the one servedA CDN, host or plugin may add its own policy. Compare the header in DevTools with your config.
Two Content-Security-Policy headersBrowsers enforce every policy, so the strictest wins. Remove the duplicate or allow MetricRaven in both.
Only connect-src errorsThe script loads but can't send visits: add https://metricraven.com (or your proxy origin) to connect-src.
Report-Only headerA Content-Security-Policy-Report-Only header only reports; it never blocks. Look for the enforcing header.
No CSP errors and still no visitsSee Troubleshooting: domains, exclusions and blockers.

Details

  • The tracker doesn't use eval, inline scripts or inline styles, so it needs no 'unsafe-inline' or 'unsafe-eval'.
  • Crawler tracking with @metricraven/ai-crawl runs on your server and isn't affected by CSP.