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
Open your live site with DevTools
Open the page in Chrome, Edge or Firefox and open DevTools (
F12, orCmd+Option+Ion a Mac).Look in the Console
A CSP block reads like this:
ConsoleRefused 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-srcblocks the script. The second meansconnect-srcblocks the requests that send visits.Find the policy
In the Network tab, select the page document and look for a
Content-Security-Policyresponse header, or search the page source forhttp-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:
script-src 'self' https://metricraven.com; connect-src 'self' https://metricraven.com;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
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:
{
"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: 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
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:
<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
# public/_headers (or the publish directory)
/*
Content-Security-Policy: script-src 'self' https://metricraven.com; connect-src 'self' https://metricraven.com;# 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>:
<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
# 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 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 (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:
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?
| Symptom | Check |
|---|---|
| The header you changed isn't the one served | A CDN, host or plugin may add its own policy. Compare the header in DevTools with your config. |
| Two Content-Security-Policy headers | Browsers enforce every policy, so the strictest wins. Remove the duplicate or allow MetricRaven in both. |
| Only connect-src errors | The script loads but can't send visits: add https://metricraven.com (or your proxy origin) to connect-src. |
| Report-Only header | A Content-Security-Policy-Report-Only header only reports; it never blocks. Look for the enforcing header. |
| No CSP errors and still no visits | See 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.