Engineering
Tracing a browser bug back to its source file.
A tester clicks on something broken on a deployed website. Your developer opens the ticket an hour later and has to find the code. That gap is where most of the time goes, and it is a solvable problem. Here is how we closed it, including the parts that do not work.
The naive idea, and why it fails
The obvious approach is source maps. You have a click, you have a stack, you resolve it back to the original file. Except there is no stack: nothing threw. The tester clicked a button that did the wrong thing, or a label that reads wrong, or a layout that broke. There is no error to map.
What you do have is the DOM node under the cursor. So the question becomes: given an element on a live page, what file authored it?
Frameworks leave their names on the DOM
Vue attaches the component instance to the element itself. Walking it gives you the component name, and the modern SFC compiler fills __name from the filename.
function detectVueComponent(el) {
const instance = el.__vueParentComponent ?? el.__vue__?.$options
if (!instance) return null
return instance.type?.name || instance.type?.__name || instance.name || null
}React hides the same information behind a fiber key that carries a random suffix, so you enumerate the element's own properties, then walk up the tree until a node has a usable name.
function detectReactComponent(el) {
for (const key of Object.keys(el)) {
if (!key.startsWith('__reactFiber$')) continue
let current = el[key]
while (current) {
const name = current.type?.displayName || current.type?.name
if (name && name !== 'Fragment') return name
current = current.return
}
}
return null
}And then the minifier eats them
This is the part nobody mentions. In development both branches work beautifully. In production they diverge sharply.
Vue survives, because the SFC compiler writes __name as a string property derived from the filename, and a minifier has no reason to touch a string. React usually does not: type.name is a function name, and mangling function names is exactly what minifiers do unless you opt out with keep_fnames. You get back t.
Which matters, because production is precisely where bugs get reported. A feature that only works on localhost solves nothing.
The fallback is better than the primary
The escape hatch turns out to be sturdier than the mechanism it rescues. Test attributes survive minification, because they are strings in the markup: data-testid, data-cy, aria-label. So does the literal text a component renders.
A data-testid="checkout-submit" is a far better search key than a component name: it is unique, it is stable across builds, and it appears verbatim in exactly one file. Teams that invested in test identifiers get better bug tracing for free, which is a pleasing incentive.
Finding the file without a code search
The instinct is to reach for the code search API. We did not, for two reasons: it is heavily rate limited, and it only indexes the default branch, which is not necessarily what was deployed.
One call to the Git trees endpoint returns every path in the repository. No rate limit worth worrying about, no indexing delay, and it accepts any ref, including the exact commit that was live. Matching then happens locally, which is both faster and cheaper.
Ranking, or: how to be wrong less often
A component named CheckoutForm will match several files. The test file. The story. A vendored copy under node_modules. A built artefact in dist. Scoring is what separates a useful link from a misleading one.
if (stem === wanted) score = 100
else if (stem === wanted + 's') score = 60
else if (stem.includes(wanted)) score = 40
else continue
if (path.startsWith('src/') || path.includes('/components/')) score += 10
if (path.includes('test') || path.includes('spec') || path.includes('stories')) score -= 30
score -= path.split('/').length Exact filename dominates and everything else only breaks ties. Vendored and generated directories are dropped outright. Depth is a mild penalty, because the shallower file is usually the real one. And a folder named after the component with an index file inside is the same thing under a different convention, so it scores nearly as high as an exact hit.
Never show one answer
The most important design decision was not technical. A confidently wrong file link is worse than no link: the developer opens it, finds nothing, and stops trusting the feature permanently.
So the report carries up to three candidates, labelled as likely rather than certain, and shows none at all when nothing scores. Being silent is an acceptable outcome. Being wrong is not.
Pinning the commit
One last detail that costs almost nothing and changes the result. If the page exposes the deployed commit, through a meta tag or an attribute on the script tag, the permalink points at the code as it was when the bug happened rather than at whatever the default branch has drifted into since.
Three weeks later, that is the difference between a link that helps and a link that lies.
What this is worth
None of this is exact. It is a heuristic over names, and it fails on a minified React bundle with no test attributes. But when it lands, the ticket arrives with the screenshot, the console, the failed request and a link to the file, and the first minute of debugging is already done.
The wider point is that the browser knows more about your code than we usually bother to ask. Frameworks annotate the DOM. Test attributes are already there. The information exists, it just needs to be picked up at the moment the bug is reported, when the page is still on screen.
This ships in FeedBug. Testers click on the bug, the issue lands in Linear or GitHub with the likely source file attached.