Userz

Content-Security-Policy

What a strict CSP must allow for the Userz browser SDK, per release channel — and what it does not have to allow.

TL;DR. Allow the CDN host in script-src, the API host and the upload host in connect-src, and blob: + data: in img-src. Styles need no 'unsafe-inline' and no nonce. Which hosts depends on the release channel your build points at, never on a URL you type.

Hosts per release channel

The SDK resolves its CDN and API hosts from the release channel it was built for. A production build (the npm package and the cdn.userz.ai script tag) talks to the .ai hosts; a staging build talks to the .net hosts. There is no option to point a build at another host — the channel is the only switch, so a CSP written against these rows stays true.

NameTypeDescription
productionhttps://cdn.userz.aiscript-src https://cdn.userz.ai · connect-src https://api.userz.ai
staginghttps://cdn.userz.netscript-src https://cdn.userz.net · connect-src https://api.userz.net

Production is where customer sites run. The staging row is here so a staging build under a strict CSP is explainable, not because a customer site should ever load one.

The policy, production

A complete policy for a page that embeds the SDK from the CDN. Merge each directive into the one you already send; nothing below needs to be the whole directive.

HTTP
Content-Security-Policy:
  script-src  'self' https://cdn.userz.ai;
  connect-src 'self' https://api.userz.ai https://*.r2.cloudflarestorage.com;
  img-src     'self' blob: data:;
NameTypeDescription
script-srchttps://cdn.userz.aiThe script-tag core and every chunk it fetches (widget, telemetry, replay) come from this host at an exact-version URL with a Subresource Integrity digest and crossorigin="anonymous". An npm install fetches no remote code, so a bundled app needs no entry here.
connect-src (API)https://api.userz.aiFeedback submissions, telemetry batches (fetch, keepalive fetch, and the sendBeacon fallback all share this directive), the configuration document, and presign requests.
connect-src (uploads)https://*.r2.cloudflarestorage.comScreenshots, annotated images, files and replay clips are uploaded straight from the page to a presigned object-storage URL on this host. The URL is the access gate (it expires in minutes); the host is a per-account subdomain, so the pattern is a wildcard.
img-srcblob: data:Screenshot previews in the panel are blob: URLs; screenshot capture rasterises the page through an SVG data: image before it is cropped. Omit data: and capture fails; omit blob: and the preview is blank. The capture adds no style-src requirement: it works under style-src 'self' without a single violation.

The policy, staging

Identical shape, the .net hosts. Only a staging build of the SDK reaches them.

HTTP
Content-Security-Policy:
  script-src  'self' https://cdn.userz.net;
  connect-src 'self' https://api.userz.net https://*.r2.cloudflarestorage.com;
  img-src     'self' blob: data:;

Styles: no unsafe-inline, no nonce

The widget renders inside an open shadow root and delivers its stylesheet as a constructed stylesheet (new CSSStyleSheet() adopted onto the root). Measured on Chromium under style-src 'self': the sheet applies and the page receives zero securitypolicyviolation events, while a <style> element placed beside it is refused. Brand colours are written as CSS custom properties through the CSSOM (style.setProperty), which is not a CSP sink either. So a strict style-src needs no 'unsafe-inline' and no nonce for the widget.

Where constructed stylesheets are unavailable

The SDK falls back to a <style> element that carries a nonce. Pass your page's nonce explicitly, or call createUserz from a nonced inline snippet — the SDK reads the nonce of the script that called it.

TypeScript
createUserz({
  publicKey: 'pub_...',
  csp: { nonce: document.querySelector('meta[name="csp-nonce"]')?.getAttribute('content') ?? undefined },
});

Trusted Types

Under require-trusted-types-for 'script' the chunk loader assigns script URLs through a policy named userz. If your policy also restricts policy names, allow it:

HTTP
Content-Security-Policy: require-trusted-types-for 'script'; trusted-types userz;

What you do not need

  • 'unsafe-inline' or 'unsafe-eval' in script-src: the SDK injects no inline script and evaluates no string.
  • worker-src: compression at page teardown is synchronous, in the page.
  • A wildcard *.userz.ai: the two hosts above are the whole surface.
  • A separate entry for telemetry: it ships in the same package and posts to the same API host as feedback.

Verifying

Deploy the policy, open your app, and watch the browser console for Refused to … lines naming a Userz host — each names the directive to extend. A blocked chunk fetch surfaces as one console.error from the SDK naming the chunk; the rest of the widget keeps working.

The measurements above were taken on Chromium; Firefox and WebKit run on the SDK's own browser matrix. The support promise itself is on the browser support page.