Attributes
Three of these behave in ways worth knowing before you debug them:
- An unrecognised
data-modesilently becomeson. The widget does not warn. A typo in this attribute leaves the button visible to everyone, which is the opposite of what a typo indata-mode="manual"was meant to achieve. The admin panel is stricter and rejects an invalid mode outright, so this only bites in the embed. data-consoleresponds to the exact stringoffand nothing else.data-console="false"anddata-console="0"both leave capture switched on.data-dry-runis enabled by presence.<script ... data-dry-run>is enough. The one value that turns it off again isdata-dry-run="false", which exists so a template can emit the attribute unconditionally and decide with a variable.data-consentdefers on any value it does not recognise, and logs an error saying so. This is the one attribute that fails toward doing less. A typo indata-modeleaves a button visible; a typo here would otherwise leave collection running on every visitor, so it does the opposite. An empty value is the exception and means “not set”, so a template can emit the attribute with nothing in it.
Visibility modes
data-mode decides when the launcher appears. It is a separate axis from data-dry-run, which
decides whether anything is sent, and every combination of the two is meaningful.
on shows the launcher to everyone. This is the default.
manual never shows it on its own. Call window.BurnsideSteps.show() when you want it, which
is the mode to use if you are putting feedback behind your own button or your own permission check.
query shows it once the page is loaded with ?feedback=on. The grant is then remembered in
sessionStorage, so the visitor keeps the widget as they navigate the rest of the site without
carrying the query string around. It lasts for the browser session, not forever: a new tab starts
clean.
key shows it only to visitors holding a valid access key. The widget posts the page’s whole
query string to the relay’s /api/gate, and the relay decides, so the key never has to be readable
in your page source. A successful check is remembered per project in sessionStorage. Which query
parameter carries the key, and what its value must be, are set per project on the relay: see
configuration.
JavaScript API
Once mounted, the widget exposes these methods onwindow.BurnsideSteps:
manual becomes useful.
Waiting for consent
If you run a consent banner, adddata-consent="deferred":
start() from your banner’s accept
handler:
start() is safe to call more than once, so you do not need to track whether you already have.
If you run the admin panel, tick “This site uses a consent banner” on the project instead of
editing the snippet by hand. The panel then emits the attribute in every copy of the snippet it
gives out, including the one a colleague copies next month.
That setting changes the snippet rather than being something the widget looks up, and it has to
work that way: consent is decided before the first request, so a setting the widget had to fetch
could not gate the fetch.
open(), show() and hide() do nothing before start() and log an error telling you so. That
is deliberate: your page might call open() from a route change or a keyboard shortcut rather
than because someone clicked, and treating any of those as consent would defeat the point of
asking.
What deferring actually stops
Worth being specific, because “the widget is hidden” and “the widget has not started” are different things and only one of them is a consent gate:data-mode="manual" is not a substitute. A manual widget is mounted and invisible: it has already
patched the console and read the visitor’s storage, and it is waiting for a call. Deferred means
not begun.
What a submission contains
The issue that arrives in your tracker carries:
The widget also sends its own version with every report, so an embed left on an old build is
identifiable from the issue rather than by guesswork. It looks like
2.0.0+904c2659: the package
version, then an id derived from the built file itself. Compare it with reported in
/widget-manifest.json on your relay. If they differ, that page is running an older build than the
one you are serving, and a reload picks up the current one.
Device is worth one note: the widget does not send it. The relay reads the User-Agent header off
the submission and reduces it to an operating system and a browser name, so the issue says
macOS · Safari rather than carrying a full user agent string into your tracker.
Keeping things out of the screenshot
The widget captures the visible page. That is the point of it, and it is also the reason you need a way to say “not that part”. There are two, and they work the same way underneath.data-mask, on the embed
A CSS selector list. Anything matching it is omitted from the capture:data-capture, on the element
If it is easier to mark the element than to name it in a selector, put the attribute on the element itself:data-mask uses, and the widget uses it on its own UI so the widget
never appears in its own screenshots. An element marked this way stays excluded permanently, and
data-mask will not disturb it.
What to mask
Worth being deliberate about, because a screenshot goes into your issue tracker and inherits whatever access that tracker has:- Anything showing another person’s data. Customer records, order details, email addresses in a list, a support inbox.
- Card numbers, bank details, and anything else a screenshot of a checkout page might contain.
- API keys and tokens shown in a settings page, which are easy to forget because they look like interface rather than like secrets.
What the capture does on the network
Nothing third-party, ever. A capture may request your own font files, from your own origin, and nothing else leaves the page except the report itself when the visitor presses Send. That needs stating because the library the capture is built on would go further by default. Left alone it resolves fonts through a built-in list of font hosts:fonts.googleapis.com,
fonts.gstatic.com, use.typekit.net, p.typekit.net, kit.fontawesome.com,
use.fontawesome.com and cdn.jsdelivr.net. On a site using webfonts that would mean pressing the
feedback button caused the visitor’s browser to fetch from a third party. Every one of those hosts
is excluded by name, so none of them is ever contacted.
Why fonts are embedded at all. The capture is not a raster of what the browser drew. The library
serialises your page into an SVG and has the browser lay it out a second time, in a context with no
access to your font faces. Without the face embedded, that second layout falls back to different
metrics and the text re-wraps: a live three-line paragraph was measured rendering as four. On a tool
whose whole job is a faithful record, that made every report about spacing, wrapping or typography
quietly misleading.
So faces we can reach are embedded, and the third-party hosts above are refused. If your fonts are
self-hosted, your captures are faithful and your visitors still contact nobody.
If your fonts come from one of those hosts, the text in the capture will re-wrap, exactly as it
did before. Rather than let that be silent, the report says so underneath the screenshot:
Text may wrap differently from the live page: fonts from fonts.googleapis.com could not be embedded.Self-hosting your webfonts removes the caveat and makes the captures exact. One exception, which we cannot switch off. The library always embeds icon fonts, and offers no option to stop it. If your page uses an icon font served from a CDN, a capture may fetch that one file. It is the visitor’s browser making a request to a host your page already uses, and it carries nothing about the report, but it is a request your page would not otherwise have made at that moment. If that matters for your site, serve your icon font from your own origin, which is a good idea for other reasons anyway.
The console buffer
Capture starts when the widget loads and runs until the page is unloaded, so a report includes what happened before the reporter decided to file it. It recordslog, info, warn and error, plus two things that never reach console at all:
uncaught window errors, and unhandled promise rejections. Both arrive tagged error. A widget
that only forwarded console.error would miss the two failures most worth seeing.
Three limits keep it bounded, and the oldest entries are dropped first:
The buffer wraps the page’s own
console, so anything your application or a third-party script
logs can end up attached to an issue. If your pages log anything you would not want in your
tracker, set data-console="off", which skips installing the wrapper entirely rather than
capturing and discarding.
Versions and pinning
Every relay serves the widget at two URLs, and they make deliberately different promises./widget.js is the rolling URL. It is what the install snippet gives you and what almost
everyone should use. Its contents change when the relay is upgraded, and it is served with a short
cache and revalidation, so a fix reaches browsers that already have the old copy within about a
minute.
/widget-<version>.<hash>.js is the pinned URL. The hash is derived from the file’s own bytes,
so the name changes whenever the contents do, and the file is served immutable for a year. A
browser that has it never asks for it again.
The current build is described at /widget-manifest.json:
reported is the value the widget puts in the issues it files. build is the same id on its own.
Both change whenever the widget’s code or its bundled dependencies change, and only then, so no
release step has to remember to bump anything.
The id in pinned is a different number: it is a digest of the finished file, taken after the
build id was stamped into it, which is what lets the pinned URL promise that the name is the
content. The two always move together. Read the pairing off this manifest rather than assuming it.
What pinning costs
A relay serves exactly one pinned build: the one it was deployed with. Upgrading the relay retires the previous pinned URL, and a page still pointing at it will get a 404 and load no widget at all. So pinning does not mean “this URL works forever”. It means “these exact bytes, until you choose to move”, and moving is a step you have to take yourself when the relay is upgraded. If nobody is watching for that, the rolling URL is the safer choice, which is why it is the default.Subresource Integrity
Theintegrity value in the manifest is the SRI hash of the pinned build. Copy it from
/widget-manifest.json on your own relay rather than from here: it changes with every build, so any
value written into documentation is wrong by the time you read it. Using it requires
crossorigin="anonymous", because the widget is loaded from a different origin than your page:
integrity attribute without crossorigin fails the
integrity check and the script does not run, which looks exactly like the widget being broken.
The same caveat as above applies, and harder: an SRI hash pins the bytes, so it stops matching the
moment the relay serves a different build. Use it on the pinned URL, never on the rolling one.