res-canary
Tells users when their browser or network blocks something your app needs — and gives their IT team the addresses to allow.
Your own CSP allows your vendors. A browser extension, a corporate proxy or a firewall may not. Nothing tells your app, so the user just sees a missing chat bubble.
Try it
Block a vendor and watch the real banner appear. Nothing is really blocked: the buttons
call report().
Quick start
Install, list your vendors, and call start() before any third-party script loads.
npm install thoro-ui
import { createCanary } from 'thoro-ui/res-canary'
import { mountBanner } from 'thoro-ui/res-canary/element'
const canary = createCanary({
ownPolicy: 'api.app.example', // any text unique to your own CSP
features: [
{
id: 'chat',
label: 'Support chat',
impact: "The support chat bubble won't appear.",
origins: ['https://widget.chat.example'],
probe: { type: 'script', url: 'https://widget.chat.example/loader.js' },
},
],
})
canary.start()
mountBanner(canary)
import { createCanary } from 'thoro-ui/res-canary'
import { ResCanary } from 'thoro-ui/res-canary/react'
import 'thoro-ui/res-canary/react.css'
const canary = createCanary({ ownPolicy: 'api.app.example', features: [/* as in the Web component tab */] })
canary.start()
export function Layout({ children }) {
return (
<>
<ResCanary canary={canary} variant="banner" />
{children}
</>
)
}
React 19 is an optional peer dependency. The component renders plain React DOM, so your page's CSS can
reach it; skip react.css to style it yourself.
How it works
Five signals; the most specific one wins. A status only moves up, so the order events arrive in doesn't matter.
| Signal | Status | Shown? |
|---|---|---|
| A CSP block from a policy that isn't yours (extension, proxy) | foreign-csp |
yes |
A <script>, <img>, <link>,
<video>, <audio> or <source> fails
|
load-failed |
yes |
| A startup probe fails or goes silent | load-failed |
yes |
You call canary. (fetch, WebSocket, WebRTC…)
|
load-failed |
yes |
| A CSP block from your own policy | own-csp |
no — it's your bug, so onOwnPolicy is called
|
Reference
Everything you can pass in, in four small tables.
createCanary options
features |
What you load from where (next table). |
ownPolicy |
Required. Text, RegExp or function that recognises your own CSP. |
onChange, onOwnPolicy
|
Callbacks for your telemetry. |
storage, storageKey |
Where dismissal is remembered. Default localStorage; null = this page only.
|
probeTimeout
|
Silence that counts as blocked. Default 15000; 0 turns it off. |
Each feature
id, label, impact |
Stable id, short name, one sentence on what won't work. |
origins |
CSP host-sources such as https://*.vendor.example. Also the list IT is asked to allow.
|
probe |
Optional startup check: script, style, image,
fetch or custom.
|
Web component
canary / items |
Uncontrolled (it dismisses itself) or controlled (you own the state). |
variant |
"banner" (full width) or "inline" (card, default). |
strings |
Override any text, for translations. |
| events |
res-canary-dismiss and res-canary-copy; preventDefault() stops
the uncontrolled dismiss.
|
React
canary / items |
Same two modes as the element. |
variant, strings, className |
Same as the element, plus a class on the root. |
lang |
Locale for joining labels; pass it when you render on a server. |
onDismiss, onCopy |
Called when × or Copy for IT is pressed. |
Theming
Set --thoro-bg, --thoro-fg, --thoro-border,
--thoro-accent, --thoro-radius or --thoro-font on
:root for every component, or on one component.
:root {
--thoro-accent: #c2410c;
}
thoro-res-canary,
.thoro-res-canary {
--thoro-bg: #fff7ed;
}
Limits
What it can't see, and what to know.
Not detected
- Iframes — browsers don't report their load failures reliably.
- Resources inside a shadow root — use a probe or
report(). fetch, XHR, WebSocket, WebRTC — callreport(id)from your error handling.- An extension that hides an element without blocking its request.
Good to know
- Call
start()early: blocks before it are missed (probes still catch them). load-failedalso covers vendor outages, so the default text says "usually".-
Chrome logs an "unused preload" warning for
scriptandstyleprobes; afetchprobe avoids it.