What gets captured
Exactly what a report carries, what it never carries, and the rule that decides.
You are putting this on a client's production site. This page is the whole list, written so you can repeat it to them.
Never captured
- Request and response headers
- Cookies
- Query strings on requests. Every recorded request URL is stored as origin plus path. Query strings are where tokens live, so they are dropped before anything is stored
console.log. Onlywarnanderrorare wrapped. Logs routinely carry customer data and would crowd out real warnings- Anything from a
data:,blob:orfile:URL. These carry their whole payload in what looks like a URL, and recording one would smuggle page content past the masking rules
There is no setting that turns any of these on.
Captured always
- Browser, OS, viewport, pixel ratio, language, the raw user-agent string
- The page URL, with its query string and fragment, because
/search?q=xis often the bug. The value of any parameter whose name contains one of your redacted keys is replaced in the browser, so a reset link's?token=or a sign-in's#access_token=never leaves it. The same goes for such a parameter inside a return address, as written or encoded once (?next=%2Fjoin%3Ftoken%3D…), and for a;jsessionid=in the path. A return address encoded twice is kept as it came - JavaScript errors from your own code, with their message and stack as the browser gives them
- Console warnings and errors from your own code. An object you pass is
written out with the value under any of your redacted keys replaced, so
console.error(e.response)does not carry the request'sAuthorizationheader. Text is kept as your code wrote it. A secret inside JSON that the page stringified into a message string is not redacted, as in"save failed " + JSON.stringify(body)or anErrorbuilt from a response, and neither istoken=abcin a message. Log the object rather than its string, or keep secrets out of what you print - Failed requests: method, URL, status, duration
- A screenshot, with passwords and your masked selectors blurred wherever it draws them: in the page, in any same-origin frame and in any open shadow root
- The element a reporter points at, if they point at one: a selector for it, where it sits on the page, and its text, which is left out whenever any of it is under one of your masked selectors
Console and error buffers hold the most recent 50 entries each.
Captured only if you ask
Request and response bodies are off by default. A project that turns them on
gets them for failed requests only, to the page's own origin or your
configured API only, redacted in the browser and truncated to 2 kB. A JSON
body loses the value under any redacted key at any depth; any other body loses
its key=value and "key": "value" pairs. JSON carried as a string inside a
body is not read. See
Project settings.
"Your code" versus "someone else's"
There is no blacklist of analytics vendors, because a list like that covers a fraction of the web and rots within a year. One rule decides instead.
Something is first-party if it comes from the page's own origin, from
anything sharing its registrable domain (so cdn.acme.com counts for
www.acme.com), or from one of the project's configured API
origins.
What that changes:
| First-party | Third-party | |
|---|---|---|
| Console warnings, JS errors | Kept | Counted, not kept |
| Failed requests | Kept | Kept |
A third party's script chattering in the console is noise. A third party's request failing is evidence, and might be the bug, so failures are never filtered by ownership.
When warnings are left out, the report says so: "12 further warnings came from third-party scripts and were not recorded." Nothing is dropped silently.
Development noise
Bundler plumbing is dropped before it reaches the buffer: Vite's /@fs/ and
/@vite/, /node_modules/, webpack hot updates. It shares your app's origin
but is never anyone's bug.
A real admin app produced 18 font 403s from Vite's module server, which read as "18 failed requests" on a report where nothing was actually failing, and evicted the API calls that mattered out of a 50-entry buffer.
Screenshots
Screenshots are taken in the browser, uploaded to private storage, and never served from it directly. The bucket is not public, and its signed links last minutes.
In the app you see them through your own session. A ticket links them through this app instead, because a storage signature lasts at most seven days and an issue outlives that. Each of those links opens one image and carries no key to anything else, including the reporter's own status page.
Masking happens before the image exists. The widget blurs the elements, takes the picture, then puts the page back.
The reporter's link
Someone who files a report gets a link like /r/pf3n8qk2mxv9wdhe. It shows the
status, the team's updates, and a box to reply. There is no account and no
password. The token in the URL is all it takes, which is why it is long and
unguessable.
Anyone with that link can read that one report and comment on it. Treat it the way you would treat a private document link. It is not in the ticket your team receives, so making an issue public does not make the reporter's page public with it.
The page also asks search engines not to index it, and so does every page in the app that shows a report, because an unguessable link can still end up in public. These links get pasted into tickets, and a ticket on a public repository is a page a crawler reads and follows.