Content Security Policy (CSP)
If your site sets a Content-Security-Policy HTTP header, you need to whitelist Datsnip. This page explains exactly which directives to add — and which approach to use depending on your CSP style.
Do I have a CSP?
Open your browser DevTools → Network → click any page request → look for a Content-Security-Policy response header. If you see one, follow the guide below.
If you don't have one (most WordPress, Webflow, or Shopify sites don't), you can skip this page entirely.
What Datsnip needs
Datsnip requires two CSP permissions:
| Directive | Why |
|---|---|
connect-src | The SDK makes fetch and sendBeacon calls to your Datsnip endpoint to load rules and send analytics. |
style-src | The SDK injects <style> tags to hide elements and show the visual picker highlight. |
Option 1 — unsafe-inline style (simplest)
This is the right choice if your site uses a keyword-based CSP (e.g. style-src 'self' 'unsafe-inline').
Add your Datsnip endpoint to connect-src, and make sure style-src includes 'unsafe-inline':
Content-Security-Policy:
connect-src https://api.datsnip.com;
style-src 'self' 'unsafe-inline';
https://api.datsnip.com is the Datsnip endpoint: it serves the SDK file and receives events, and it is the ingestUrl in your snippet.
Option 2 — Nonce-based CSP (recommended for security-conscious sites)
If your site uses a nonce-based CSP (style-src 'nonce-...') — common on SaaS products, finance, and enterprise sites — use this approach instead. It avoids adding 'unsafe-inline' entirely.
How it works
Your server generates a fresh random nonce on every page request and puts it in the CSP header and on the script tags. Datsnip reads the nonce from its own <script> tag and automatically stamps it on every <style> tag it injects, so the browser accepts them.
Step 1 — Generate a nonce on your server
The nonce must be a cryptographically random base64 string, regenerated for every request.
// Node.js example
const crypto = require('crypto')
const nonce = crypto.randomBytes(16).toString('base64')
# Python example
import secrets, base64
nonce = base64.b64encode(secrets.token_bytes(16)).decode()
Step 2 — Set the CSP header
Content-Security-Policy:
connect-src https://api.datsnip.com;
style-src 'nonce-NONCE_VALUE';
script-src 'nonce-NONCE_VALUE';
Step 3 — Add the nonce to your snippet
<!-- Anti-flicker style — include nonce here too -->
<style nonce="NONCE_VALUE" id="_gs_af">html{visibility:hidden;pointer-events:none}</style>
<noscript><style nonce="NONCE_VALUE">#_gs_af{visibility:visible!important}</style></noscript>
<script nonce="NONCE_VALUE">setTimeout(function(){var e=document.getElementById('_gs_af');if(e)e.remove()},3000)</script>
<!-- Datsnip SDK — set the config first; the nonce on this tag is what the SDK reads automatically -->
<script nonce="NONCE_VALUE">
window._sf = { siteId: 'YOUR_SITE_ID', ingestUrl: 'https://api.datsnip.com' }
</script>
<script nonce="NONCE_VALUE" src="https://api.datsnip.com/sdk/tracker.js" async></script>
That's it — no extra configuration. The SDK reads the nonce from its own <script> tag and uses it on every <style> tag it injects.
The SDK captures document.currentScript.nonce synchronously when it first loads (before any async operations), then applies it to every <style> element it creates — including the hidden-elements stylesheet and the visual picker overlay.
Framework-specific examples
Next.js App Router
Next.js has built-in nonce support via its CSP middleware guide.
In middleware.ts:
import { NextResponse } from 'next/server'
import crypto from 'crypto'
export function middleware() {
const nonce = Buffer.from(crypto.randomUUID()).toString('base64')
const csp = `
script-src 'nonce-${nonce}' 'strict-dynamic';
style-src 'nonce-${nonce}';
connect-src https://api.datsnip.com;
`.replace(/\s{2,}/g, ' ').trim()
const res = NextResponse.next()
res.headers.set('Content-Security-Policy', csp)
res.headers.set('x-nonce', nonce) // pass nonce to your layout
return res
}
In app/layout.tsx, read the nonce from the request header and put it on your Datsnip script tags:
import { headers } from 'next/headers'
export default function RootLayout({ children }) {
const nonce = headers().get('x-nonce') ?? ''
return (
<html>
<head>
<script nonce={nonce} dangerouslySetInnerHTML={{
__html: `window._sf = { siteId: 'YOUR_SITE_ID', ingestUrl: 'https://api.datsnip.com' }`
}} />
<script nonce={nonce} src="https://api.datsnip.com/sdk/tracker.js" async />
</head>
<body>{children}</body>
</html>
)
}
Express / Node.js
app.use((req, res, next) => {
const nonce = require('crypto').randomBytes(16).toString('base64')
res.locals.nonce = nonce
res.setHeader('Content-Security-Policy', [
`script-src 'nonce-${nonce}'`,
`style-src 'nonce-${nonce}'`,
`connect-src https://api.datsnip.com`,
].join('; '))
next()
})
Then in your template:
<script nonce="<%= nonce %>">
window._sf = { siteId: 'YOUR_SITE_ID', ingestUrl: 'https://api.datsnip.com' }
</script>
<script nonce="<%= nonce %>" src="https://api.datsnip.com/sdk/tracker.js" async></script>
Troubleshooting
Hidden elements aren't being hidden
The most common cause is a style-src violation blocking the injected stylesheet. Open DevTools → Console and look for a CSP error mentioning style-src. Fix it using Option 1 or 2 above.
No analytics data arriving
This is almost always a missing connect-src. Open DevTools → Network, look for failed requests to your Datsnip endpoint, then add it to connect-src.
The visual picker highlight doesn't appear
Same as hidden elements — the _gs_picker_style tag is blocked by style-src. Apply Option 1 or 2.
I'm using a hash-based CSP ('sha256-...')
Hash-based policies are not compatible with Datsnip because the SDK injects styles with dynamic content (selectors and colours change per-rule). Use the nonce approach instead.