OpenReplay CSP Mistakes That Break Session Replay
Table of Contents
OpenReplay is one of those tools that looks simple until CSP gets involved. You add the replay snippet, refresh the page, and nothing happens. No sessions, no network activity, maybe a vague console error if you are lucky.
I’ve seen teams burn hours on this because the OpenReplay install itself is fine. The problem is usually CSP drift: a policy that made sense before replay was added, but now blocks the exact things replay needs.
Here are the mistakes I see most often, what they look like in production, and how to fix them without turning your CSP into *-flavored nonsense.
Mistake #1: Allowing the script but forgetting the data pipeline
A lot of developers stop after script-src. The OpenReplay loader downloads, so they assume they’re done. Then replay still fails because the browser blocks the outbound data stream.
Session replay tools usually need more than one directive:
script-srcfor the loaderconnect-srcfor API calls, beacon uploads, and WebSocket traffic- sometimes
worker-srcif the SDK uses workers - sometimes
img-srcif fallback beacons or pixel-style requests are involved
For OpenReplay, connect-src is where people usually trip up.
Broken policy
Content-Security-Policy:
default-src 'self';
script-src 'self' https://cdn.openreplay.com;
This lets the script load, but the replay client can’t send anything.
Fix
Add the actual OpenReplay ingest and WebSocket endpoints to connect-src.
Content-Security-Policy:
default-src 'self';
script-src 'self' https://cdn.openreplay.com;
connect-src 'self' https://api.openreplay.com wss://replay.openreplay.com;
Your exact domains may differ if you are self-hosting OpenReplay. That is another place people get burned: they copy the cloud example and forget their own collector hostname.
If you want ready-made policy patterns to start from, csp-examples.com is useful for comparing common CSP layouts before you adapt them to your environment.
Mistake #2: Forgetting WebSocket schemes
This one is sneaky because developers add the hostname but not the right scheme.
They’ll write this:
connect-src 'self' https://replay.example.com;
But the SDK actually opens a WebSocket connection:
new WebSocket("wss://replay.example.com/ws");
https:// does not cover wss://. CSP treats them separately.
Fix
If OpenReplay uses WebSockets in your setup, explicitly allow wss: for the right host.
Content-Security-Policy:
default-src 'self';
connect-src 'self' https://replay.example.com wss://replay.example.com;
I like checking this in a CSP scanner before shipping. A real-world header from headertest.com shows the pattern clearly:
content-security-policy: default-src 'self' https://www.googletagmanager.com https://*.cookiebot.com https://*.google-analytics.com; script-src 'self' 'nonce-MDFmNGExMjktYTg5Ni00MjgxLThkNjctY2ViODM5MTA0OGE4' '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'
Notice how connect-src includes both https://or.headertest.com and wss://or.headertest.com. That is exactly the kind of detail people miss with session replay.
Mistake #3: Copy-pasting a cloud CSP for a self-hosted OpenReplay install
OpenReplay is often self-hosted, which is great for control and data residency. It also means the CSP examples from docs or blog posts often do not match reality.
I’ve seen setups where the script comes from one domain, the API runs on another, and the WebSocket endpoint is behind a different subdomain entirely. Then somebody whitelists only app.example.com and expects everything to work.
Fix
Map the actual request graph in DevTools. Don’t guess.
Open the page, filter network requests by openreplay, ws, and XHR/fetch, then build your CSP from the real hosts you see.
Example self-hosted policy:
Content-Security-Policy:
default-src 'self';
script-src 'self' https://static.example.com;
connect-src 'self' https://openreplay-api.example.com wss://openreplay-ws.example.com;
img-src 'self' data: https:;
If you serve the tracker from your own app origin, even better:
Content-Security-Policy:
default-src 'self';
script-src 'self';
connect-src 'self' https://openreplay-api.example.com wss://openreplay-ws.example.com;
That is cleaner and easier to reason about.
Mistake #4: Using nonces, but not on the OpenReplay bootstrap script
A strict CSP with nonces is the right move. The mistake is adding a nonce-based policy and then injecting the OpenReplay snippet without the nonce attribute.
Broken HTML
<script>
window.OPENREPLAY_PROJECT_KEY = "xyz";
// OpenReplay bootstrap code here
</script>
Broken CSP
Content-Security-Policy:
default-src 'self';
script-src 'self' 'nonce-rAnd0m123';
The inline bootstrap script gets blocked because it lacks the matching nonce.
Fix
Attach the nonce server-side.
<script nonce="{{ .CSPNonce }}">
window.OPENREPLAY_PROJECT_KEY = "xyz";
</script>
<script nonce="{{ .CSPNonce }}" src="/js/openreplay-bootstrap.js"></script>
Or better, move as much bootstrap logic as possible into external scripts and keep inline code minimal.
If you use strict-dynamic, be deliberate about it. It can simplify script loading chains, but it also changes trust behavior in ways teams often don’t fully understand. I’m a fan of it when the app is already engineered around nonces. I’m not a fan of sprinkling it into a legacy app and hoping for the best.
Mistake #5: Whitelisting too broadly because replay was “urgent”
The panic fix usually looks like this:
script-src * 'unsafe-inline' 'unsafe-eval';
connect-src *;
Yes, the replay starts working. You also just gutted most of the point of having CSP.
Session replay integrations do not require a reckless policy. They require an accurate one.
Fix
Allow only the exact origins OpenReplay needs. Start from a report-only policy if you want a safer rollout.
Content-Security-Policy-Report-Only:
default-src 'self';
script-src 'self' https://cdn.openreplay.com;
connect-src 'self' https://api.openreplay.com wss://replay.openreplay.com;
report-to default-endpoint;
Watch violations, confirm behavior, then promote it to enforcing mode.
Mistake #6: Forgetting worker-src in stricter environments
Not every OpenReplay deployment needs this, but some modern SDKs or surrounding app code use workers for buffering, compression, or processing. If your policy is strict and you only define default-src, browser behavior may not match what you expect.
Symptom
The replay script loads, network permissions look fine, but some part of the client silently fails with CSP errors mentioning worker-src or blob workers.
Fix
If needed, explicitly set worker-src.
Content-Security-Policy:
default-src 'self';
script-src 'self' https://cdn.openreplay.com;
connect-src 'self' https://api.openreplay.com wss://replay.openreplay.com;
worker-src 'self' blob:;
Don’t add blob: unless you actually need it. But if the SDK or your build output relies on workers, this is worth checking early.
Mistake #7: Ignoring CSP violations because replay “mostly works”
“Mostly works” is dangerous with session replay. You might capture some sessions but lose chunks of data, miss live updates, or fail on specific browsers.
A blocked WebSocket reconnect or a denied endpoint for one route can leave you with partial recordings that are hard to trust.
Fix
Turn on reporting.
Content-Security-Policy:
default-src 'self';
script-src 'self' https://cdn.openreplay.com;
connect-src 'self' https://api.openreplay.com wss://replay.openreplay.com;
report-uri https://csp-report.example.com/report;
Or use report-to if your reporting pipeline supports it.
Then actually read the reports. I know that sounds obvious, but a lot of teams wire up CSP reporting and never look at it again.
Mistake #8: Mixing environments without environment-specific CSP
Staging often uses different OpenReplay hosts than production. Local development may proxy everything through one origin. Production may split static assets, API, and sockets.
If you try to force one identical CSP across all environments, one of them usually ends up wrong.
Fix
Generate CSP per environment.
For example:
add_header Content-Security-Policy "default-src 'self'; script-src 'self' https://cdn.openreplay.com; connect-src 'self' https://api.openreplay.com wss://replay.openreplay.com" always;
And in staging:
add_header Content-Security-Policy "default-src 'self'; script-src 'self' https://staging-cdn.openreplay.com; connect-src 'self' https://staging-api.openreplay.internal wss://staging-replay.openreplay.internal" always;
Boring? Yes. Correct? Also yes.
A practical baseline policy
If you are integrating OpenReplay and want a sane starting point, this is the shape I’d begin with and then tighten based on your exact deployment:
Content-Security-Policy:
default-src 'self';
script-src 'self' https://cdn.openreplay.com;
connect-src 'self' https://api.openreplay.com wss://replay.openreplay.com;
img-src 'self' data: https:;
style-src 'self' 'unsafe-inline';
object-src 'none';
base-uri 'self';
frame-ancestors 'none';
form-action 'self';
If you self-host OpenReplay, replace those domains with your real ones. If you use nonces, use them consistently. If your app depends on workers, define worker-src. And if you’re tempted to solve it with wildcards, stop and look at the actual requests first.
That’s the pattern with CSP and session replay every single time: the fix is almost never “make it looser.” The fix is “make it accurate.”