CSP for Datadog RUM and Synthetics: A Real-World Fix
Table of Contents
I’ve seen this pattern a lot: a team adds Datadog RUM, turns on Synthetic Monitoring, and suddenly the browser console lights up with CSP violations. The app still loads, mostly. Dashboards stay empty, session replay breaks, synthetic tests look flaky, and everyone starts guessing which domain needs to be whitelisted.
Guessing is how CSP turns into a junk drawer.
Here’s a cleaner way to do it, based on a realistic setup and the exact kinds of changes teams end up making in production.
The setup
A marketing site on csp-guide already had a decent CSP. Not perfect, but decent: nonce-based scripts, strict-dynamic, no object-src, and a reasonably tight connect-src.
A real example of a modern CSP header looks like this:
content-security-policy: default-src 'self' https://www.googletagmanager.com https://*.cookiebot.com https://*.google-analytics.com; script-src 'self' 'nonce-YWNkNzMzNWItODc2NS00OWNlLWE5OGUtOGFjYThmNGRhZjlk' 'strict-dynamic' https://www.googletagmanager.com https://*.cookiebot.com https://*.google-analytics.com; style-src 'self' 'unsafe-inline' https://www.googletagmanager.com https://*.cookiebot.com https://consent.cookiebot.com; img-src 'self' data: https:; font-src 'self'; connect-src 'self' https://api.headertest.com https://tallycdn.com https://or.headertest.com wss://or.headertest.com https://*.google-analytics.com https://*.googletagmanager.com https://*.cookiebot.com; frame-src 'self' https://consentcdn.cookiebot.com; frame-ancestors 'none'; base-uri 'self'; form-action 'self'; object-src 'none'
That’s from HeaderTest, and it’s a good reminder that most real CSPs are already balancing analytics, consent tooling, and app traffic before Datadog enters the picture.
Then the team added:
- Datadog Browser RUM
- Session Replay
- Synthetic browser tests against production
And things broke in subtle ways.
What broke first
The first symptom was easy to miss: RUM events stopped reaching Datadog. In DevTools, they saw CSP errors like:
Refused to connect to 'https://browser-intake-datadoghq.com/api/v2/rum?...'
because it violates the following Content Security Policy directive:
"connect-src 'self' https://api.headertest.com ..."
Then session replay partially failed because more Datadog intake endpoints were used than expected.
Later, synthetic tests started flagging issues around script loading and cross-origin requests. Not because Datadog Synthetics injects code into your app in the way people often assume, but because your page’s runtime behavior under test still depends on the CSP allowing the necessary monitoring traffic.
The team’s first reaction was the classic one:
connect-src 'self' https: wss:;
script-src 'self' 'unsafe-inline' 'unsafe-eval' https:;
img-src * data:;
That “fixes” the outage by gutting the policy. I hate this move because it always becomes permanent.
Before: a CSP that worked until Datadog
Here’s a simplified version of the pre-Datadog policy:
Content-Security-Policy:
default-src 'self';
script-src 'self' 'nonce-{RANDOM}' 'strict-dynamic' https://www.googletagmanager.com https://*.cookiebot.com https://*.google-analytics.com;
style-src 'self' 'unsafe-inline' https://consent.cookiebot.com https://*.cookiebot.com;
img-src 'self' data: https:;
font-src 'self';
connect-src 'self' https://api.example.com https://*.google-analytics.com https://*.googletagmanager.com;
frame-src 'self' https://consentcdn.cookiebot.com;
frame-ancestors 'none';
base-uri 'self';
form-action 'self';
object-src 'none';
This is a pretty normal baseline for a content-heavy site.
Then they added the standard Datadog RUM snippet:
<script nonce="{{ .CSPNonce }}">
window.DD_RUM && window.DD_RUM.init({
applicationId: "app-id",
clientToken: "client-token",
site: "datadoghq.com",
service: "csp-guide",
env: "prod",
sessionSampleRate: 100,
sessionReplaySampleRate: 20,
trackResources: true,
trackLongTasks: true,
trackUserInteractions: true
});
</script>
<script
nonce="{{ .CSPNonce }}"
src="https://www.datadoghq-browser-agent.com/us1/v6/datadog-rum.js">
</script>
And that introduced two CSP requirements:
script-srcmust allow the Datadog browser agent host, unless your nonce/loader pattern already covers it safely.connect-srcmust allow Datadog intake endpoints used by RUM and replay.
The wrong fix
The first patch looked like this:
script-src 'self' 'unsafe-inline' https://*.datadoghq.com https://www.datadoghq-browser-agent.com;
connect-src *;
img-src * data:;
This is what happens when someone is under pressure and only wants the console errors gone.
Problems with this patch:
connect-src *is way broader than needed'unsafe-inline'undoes a lot of the benefit of nonce-based scriptinghttps://*.datadoghq.comis often too vague to be useful and sometimes still incomplete depending on region and endpoint usage- It normalizes “just allow everything” as the fix pattern
The real fix
The better approach was to identify exactly what Datadog features were enabled and only add the sources they needed.
For most Browser RUM deployments, you’ll usually need:
- The browser agent script host
- Datadog intake endpoints in
connect-src
A practical after-policy looked like this:
Content-Security-Policy:
default-src 'self';
script-src 'self' 'nonce-{RANDOM}' 'strict-dynamic' https://www.googletagmanager.com https://*.cookiebot.com https://*.google-analytics.com https://www.datadoghq-browser-agent.com;
style-src 'self' 'unsafe-inline' https://consent.cookiebot.com https://*.cookiebot.com;
img-src 'self' data: https:;
font-src 'self';
connect-src 'self'
https://api.example.com
https://*.google-analytics.com
https://*.googletagmanager.com
https://browser-intake-datadoghq.com
https://session-replay.browser-intake-datadoghq.com;
frame-src 'self' https://consentcdn.cookiebot.com;
frame-ancestors 'none';
base-uri 'self';
form-action 'self';
object-src 'none';
That solved the immediate issue without blowing the doors off the policy.
A region gotcha that bites people
Datadog is regional. If your org uses EU or another site, the endpoints differ. I’ve seen teams copy a US-based CSP allowlist and then spend an hour wondering why nothing works in production.
Your site setting in the Datadog init config matters:
window.DD_RUM.init({
clientToken: "client-token",
applicationId: "app-id",
site: "datadoghq.eu"
});
If the site changes, your CSP sources probably need to change too.
Don’t cargo-cult hostnames from random docs or blog posts. Verify the actual blocked URLs in DevTools and in your CSP reports.
Session Replay usually expands connect-src
If you enable replay, expect extra intake traffic. That’s where a lot of “RUM works, replay doesn’t” bugs come from.
A good rule: treat Datadog features separately.
- RUM core
- Session Replay
- Logs, if using browser logs
- Synthetic test behavior you rely on
Each may add network destinations. Keep them explicit.
Synthetic Monitoring and CSP
Synthetics confuses people because they expect a browser test to need some magical CSP bypass. That’s usually not the problem.
What actually happens is simpler:
- The page is loaded in a real browser context
- Your CSP still governs what the page can load and where it can connect
- If your app depends on Datadog RUM, analytics, consent tools, APIs, or WebSockets, the test sees the same breakage users would
That’s good. It means synthetics is exposing a real production policy issue.
Where teams get tripped up is when they harden CSP for human traffic but forget the app under synthetic test still needs the same third-party telemetry connections. If RUM is part of the page, the test browser will hit those restrictions too.
A safer rollout pattern
The team didn’t push the final CSP straight to enforcement. They used Report-Only first:
Content-Security-Policy-Report-Only:
default-src 'self';
script-src 'self' 'nonce-{RANDOM}' 'strict-dynamic' https://www.datadoghq-browser-agent.com;
connect-src 'self' https://browser-intake-datadoghq.com https://session-replay.browser-intake-datadoghq.com;
report-to csp-endpoint;
That gave them a clean list of actual violations before they touched the enforced header.
This is the only sane way to add vendors to a nontrivial CSP.
Before and after: the diff that mattered
Before:
script-src 'self' 'nonce-{RANDOM}' 'strict-dynamic' https://www.googletagmanager.com https://*.cookiebot.com https://*.google-analytics.com;
connect-src 'self' https://api.example.com https://*.google-analytics.com https://*.googletagmanager.com;
After:
script-src 'self' 'nonce-{RANDOM}' 'strict-dynamic' https://www.googletagmanager.com https://*.cookiebot.com https://*.google-analytics.com https://www.datadoghq-browser-agent.com;
connect-src 'self' https://api.example.com https://*.google-analytics.com https://*.googletagmanager.com https://browser-intake-datadoghq.com https://session-replay.browser-intake-datadoghq.com;
That’s it. Small, targeted, boring. Exactly what you want from a CSP change.
What I’d do on a real team
My checklist is pretty simple:
- Add Datadog in a staging environment with CSP Report-Only enabled.
- Capture the exact blocked hosts from DevTools and reports.
- Allow only the Datadog script and intake hosts you actually use.
- Keep nonce-based
script-src; don’t regress to'unsafe-inline'. - Re-test Session Replay separately from basic RUM.
- Run synthetic browser tests after the CSP update, not before signing off.
- Re-check the final policy with a tool and keep examples documented for future changes.
If you want examples of tighter policies without falling back to wildcards, csp-examples.com is handy for ready-to-use patterns.
The main lesson from this case wasn’t “Datadog needs these domains.” It was that observability tooling has a habit of pressuring teams into broad CSP exceptions. Resist that pressure. Add the smallest possible allowances, keep them region-aware, and treat every new vendor integration like a policy change, not just a script tag.