Instrument urls built from a JavaScript undefined (BL-16666) - #8175
Open
JohnThomson wants to merge 8 commits into
Open
Instrument urls built from a JavaScript undefined (BL-16666)#8175JohnThomson wants to merge 8 commits into
JohnThomson wants to merge 8 commits into
Conversation
Sentry has years of "Cannot Find File" reports for paths like
Temp/undefined. The old card framed these as C# building a temp path from a
name that was undefined; that is wrong, and a guard there would catch
nothing. What happens is that some front-end code does the equivalent of
`element.src = aVariableThatIsUndefined`: the DOM turns the value into the
text "undefined" and asks the server for a file by that name. The server can
only report that a file called "undefined" is missing, which never says who
asked - so finding BL-16447, the one instance we have fixed, took a lot of
guessing.
This does not fix any particular instance. It makes the next one name
itself, in two layers.
Server side: carry the HTTP Referer through to the report, so a bogus
request names the page that issued it. "undefined", "null" and "NaN" are
matched as a whole path segment, case-sensitively, so a real file called
undefined.png or a folder called undefinedThings is untouched. We also stop
suppressing these reports when the path is under the current book folder.
That suppression is what has been hiding most instances, since a bogus url
built by a book page resolves inside that book's folder - so expect more of
these reports, not fewer. That is the intent; they are now actionable.
Front end: a new lib/undefinedUrlDetector, installed from lib/errorHandler
because every bundle's root module already imports it. It intercepts src
assignment, setAttribute("src"), fetch and XMLHttpRequest.open and reports
with a JavaScript stack, which names the offending line. It is deliberately
passive - it reports and lets the assignment proceed - and reports each
distinct problem once, capped per session, because these bugs live in
components that re-render.
Two things worth knowing for anyone changing this later. The check takes the
value rather than a string because at `img.src = x` the value arrives as the
real undefined; the DOM stringifies it inside the native setter, and by the
time a string exists it is too late to know who set it. And fetch/XHR needed
wrapping because BL-16447 came in through WaveSurfer fetching the url rather
than putting it on an element.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…url-instrumentation # Conflicts: # src/BloomExe/web/BloomServer.cs
…16666) Two things Devin caught on the first pass. Keep the new reports quiet. ShouldReportFailedRequest deliberately stopped suppressing requests whose path contains a JavaScript value, so that we finally see them. But the report they fall through to uses PassiveIf.All and ModalIf.Beta, which means a toast on every channel and a modal on beta - so un-suppressing them would have started interrupting ordinary users on pages that previously failed silently. There is nothing a user can do about our bug, so this class now reports with ModalIf.None/PassiveIf.None: still logged and still sent to Sentry, but invisible to the user. It also gets its own short message, so it forms its own Sentry issue instead of being buried in the general "Cannot Find File" one. Fix the spec assertion. When the card references were repointed from BL-16577 to BL-16666, the message in describeBadUrl changed but the spec's assertion did not, so that test could never pass. The C# suite was re-run after that rename but the front-end suite was not, which is exactly how it slipped through. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
JohnThomson
commented
Aug 7, 2026
JohnThomson
commented
Aug 7, 2026
JohnThomson
commented
Aug 7, 2026
Devin caught that the merge left a contradiction. The JavaScript-value branch returns early, so by the time control reaches the later branches extraDiagnostics is always empty - which meant the "both kinds of detail" comment was false, the bare-name detail never reached a report about a JavaScript-value url, and the general branch had gained an argument that could never be filled. Now the early branch emits both (a bare "undefined" genuinely has no directory AND is a JavaScript value), and the later branches ask only for the bare-name detail, which is the only one they can ever have. Also records what the front-end detector does and does not cover: it patches the prototypes of the window it is installed in, so a frame gets it by running one of our bundles, but parent-frame code reaching into a child frame's document is not seen. Worth knowing before reading a silence there as proof that nothing went wrong. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
JohnThomson
commented
Aug 7, 2026
JohnThomson
commented
Aug 7, 2026
…L-16666) Devin's point, and a good one: ShouldReportFailedRequest decided from GetLocalPathWithoutQuery(info) while the quiet branch in ReportMissingFile decided from the localPath that ProcessAnyFileContent happened to be holding. They are the same string today, but they are computed independently, and if they ever diverged the consequence is exactly the thing the quiet branch exists to prevent: the request would fall through to the ModalIf.Beta/PassiveIf.All branch and interrupt the user. Both now ask IsJavascriptValueRequest(info), so they cannot disagree. ReportMissingFile takes the request rather than a pre-computed diagnostics string, which is what made the divergence possible. Also adds the missing explanatory comments on three helpers in the front-end detector, per AGENTS.md. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
JohnThomson
commented
Aug 7, 2026
JohnThomson
commented
Aug 7, 2026
…(BL-16666) Devin's point: errorHandler runs unhandled-error stacks through stacktrace-js so the report names a line in our source, but the new detector was handing over a raw stack, which points into a bundle. For a feature whose entire purpose is to name the line that built the bad url, that mostly defeats it. The detector now hands over the Error itself rather than its stack text, and errorHandler - which already owns the mapping - maps it before reporting, falling back to the raw stack if the mapping fails. Unlike window.onerror we don't send a preliminary unmapped report first: nothing is about to crash and take the mapping with it, and these are deliberately quiet diagnostics, so one report per problem is enough. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
JohnThomson
commented
Aug 7, 2026
JohnThomson
commented
Aug 7, 2026
JohnThomson
commented
Aug 7, 2026
Devin: "Only the first bad-url report of each kind is ever sent, so later problems stay invisible." Right, and it mattered. Every component that sets an image src to undefined produces the same message, so keying the report-once rule on the message meant we would report whichever component ran first and hide every other one for the rest of the session - which is precisely the blind spot this feature exists to remove. The key is now the message plus the stack. Deliberately the whole stack rather than an attempt to pick out "the caller": any rule for finding that frame has to guess which frames are ours, and a wrong guess lands on a frame that two different call sites share, silently reinstating the blind spot. I tried the clever version first and a test caught it doing exactly that. That leaves the hard cap as the real flood protection rather than the collapse, which the reworked test now says out loud - identical repeats do still collapse, but only when the stack is identical, which a re-render gives us and a caller at a different line does not. The cap bounds it either way. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
JohnThomson
commented
Aug 7, 2026
JohnThomson
commented
Aug 7, 2026
JohnThomson
commented
Aug 7, 2026
JohnThomson
marked this pull request as ready for review
August 7, 2026 15:53
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What this is
Sentry issue 2699459502 has years of "Cannot Find
File" reports for paths like
C:/Users/<user>/AppData/Local/Temp/undefined.BL-16577 framed these as C# building a temp path from a name that was
undefined. That framing iswrong, and a guard there would catch nothing. What actually happens is that some front-end code
does the equivalent of
element.src = aVariableThatIsUndefined: the DOM turns the value into thetext
"undefined"and asks the server for a file by that name. The server can only report that afile called
undefinedis missing — never who asked — which is why finding BL-16447, the oneinstance we have fixed, took so much guessing.
This does not fix any particular instance. It makes the next one name itself.
Two other things fell out of the investigation and are worth knowing:
"undefined"src resolves against the document's base url,and
ShouldReportFailedRequestsuppresses reports for anything under the current book folder — sothese bugs are invisible when they happen on a book page. Everything reaching Sentry came from
documents Bloom writes into temp and navigates to. We have been seeing a filtered slice.
src={undefined}is safe (React omits the attribute). Only the stringifying forms bite:el.src = x,setAttribute("src", x),new Audio(x), and template literals.Layer A — server side, with the referrer
Referernow flows throughIHttpListenerRequest→IRequestInfo→ the report, so a bogus requestnames the page that issued it.
undefined/null/NaNare matched as a whole path segment, case-sensitively, so a realundefined.png, a folder calledundefinedThings, or a file calledUndefinedare untouched.This will increase reported volume. These reports are no longer suppressed under the current book
folder — that suppression is exactly what was hiding most instances. That is the intent, and the
extra reports should be actionable, but whoever watches Sentry should expect it.
Layer B — front-end interception, for the stack
New
lib/undefinedUrlDetector.ts, installed fromlib/errorHandler.ts— the module every bundle'sroot already imports, so coverage is automatic. It intercepts
srcassignment,setAttribute("src"),fetchandXMLHttpRequest.open, and reports with a JavaScript stack, whichnames the offending line.
It is deliberately passive: it reports and lets the assignment proceed. The stack is what we
need and we get it either way, so there is no reason to change what the app does. (The plan called
for throwing in Debug/Alpha; I left that out for the same reason. Easy to add.)
Two things worth knowing if you change this later:
img.src = xthe value arrives as the realundefined— the DOM stringifies it inside the native setter. My first version checked stringsonly and therefore caught nothing at the assignment site; the tests caught that.
fetch/XHR had to be wrapped too. BL-16447 came in through WaveSurfer fetching the urlrather than putting it on an element, so element interception alone would have missed the one
case we actually know about.
It also reports each distinct problem once, capped at 10 per session — each report is an http post,
and these bugs live in components that re-render.
Worth a careful look
ReportMissingFile: PR Add diagnostics for missing files requested by bare name (BL-16577) #8171 (now landed) addedGetBareNameDiagnosticsto the same format call this addsextraDiagnosticsto. I kept both —they answer different questions, and a bare
undefinedrequest legitimately raises both.fetchwrapper calls through onwindow, notthis. A barefetch(url)in a module givesthis === undefined, and the browser rejects fetch invoked on anything but the window — that wouldhave broken every fetch in Bloom.
Testing
BloomServerTests,RequestInfoTests,BloomFileLocatorTests(13 new).undefinedUrlDetectorSpec; full vitest suite green.build/agent-vite.sh(worth doing — thistouches a module every bundle imports).
Ref: https://issues.bloomlibrary.org/youtrack/issue/BL-16666 (split from
https://issues.bloomlibrary.org/youtrack/issue/BL-16577)
Devin review
This change is