thoro-ui

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.report(id) (fetch, WebSocket, WebRTC…) load-failed yes
A CSP block from your own policy own-csp no — it's your bug, so onOwnPolicyViolation 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, onOwnPolicyViolation Callbacks for your telemetry.
storage, storageKey Where dismissal is remembered. Default localStorage; null = this page only.
probeTimeoutMs 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 — call report(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-failed also covers vendor outages, so the default text says "usually".
  • Chrome logs an "unused preload" warning for script and style probes; a fetch probe avoids it.