Determining which javascript script changed an element’s attribute

Determining which script changed an element's attribute

So, I had an issue where quite a while ago I added some js code that would open a details disclosure element if it contained a named anchor that was included in the page's URL. For instance if you loaded the URL https://thisdomain.com/somepage.html#blahblahblah, and the page had <a name="blahblahblah"> hidden inside a closed details element, it would open that element by setting the attribute "open" on the details element, and scroll to reveal the anchor.

The problem was, I needed to make some changes to how that code functioned, and I couldn't find where I had added the script that did that.

Long story short: I temporarily added this script to the head of the page, and then reloaded it with an #anchor added to the URL, in this case https://michaelkupietz.com/literally-hundreds-capsule-reviews/#puzzlehead:


<script type="text/javascript" data-cfasync="false">
// Override the open property setter to catch when it's being set
const originalDescriptor = Object.getOwnPropertyDescriptor(HTMLDetailsElement.prototype, 'open');
Object.defineProperty(HTMLDetailsElement.prototype, 'open', {
  set: function(value) {
    if (value === true) {
      console.trace('Details element being opened:', this);
      debugger; // This will break execution
    }
    originalDescriptor.set.call(this, value);
  },
  get: originalDescriptor.get
});

This produced the console output:

console.trace() Details element being opened:
<details class="mctag-topdetails scriptable a1" open="">
literally-hundreds-capsule-reviews:36:13801
nrWrapper https://michaelkupietz.com/literally-hundreds-capsule-reviews/:36
set https://michaelkupietz.com/literally-hundreds-capsule-reviews/:11
handleAnchor https://michaelkupietz.com/literally-hundreds-capsule-reviews/:3362
nrWrapper https://michaelkupietz.com/literally-hundreds-capsule-reviews/:36
(Async: EventListener.handleEvent)
nrWrapper https://michaelkupietz.com/literally-hundreds-capsule-reviews/:36
<anonymous> https://michaelkupietz.com/literally-hundreds-capsule-reviews/:3374

We can ignore the nrWrapper lines, those are because I'm testing New Relic, which is a whole other story.

But we see a "set" on line 11, which, if we view the page source, is actually the script line that calls the console log. However, then we see "handleAnchor" on line 3362—and that's the name of the function that is setting the details attribute "open", and the line of the page source it's doing it on. In the original function I had it in, it's wrapped in an anonymous function, hence <anonymous>, although if it had been wrapped in one or more named functions, that would have been listed here in detail. But the line 3362 showed me what to look at, and if "handleAnchor" wasn't unique enough to search my codebase for, the greater context around line 3362 would have given me enough to search for and find it.

Now, it's worth pointing out, this can be adapted without much difficulty to watch attributes on other elements. In the javascript that does this, notice HTMLDetailsElement in Object.getOwnPropertyDescriptor(HTMLDetailsElement.prototype, 'open');. HTMLDetailsElement is the prototype of the HTML details element. You need to know the prototype of whatever element you need to watch change, and those prototype names aren't always standard, either within any single browser, nor across browsers.

Fortunately you can get the prototype names. I found some code at https://kangax.github.io/jstests/html5_elements_interfaces_test/ that you can insert in the body of an HTML page to list the javascript prototypes of HTML elements:

<script type="text/javascript">

(function(){
var elements = (
'a abbr address area article aside audio ' +
'b base bdo blockquote body br button ' +
'canvas caption cite code col colgroup command ' +
'datalist dd del details device dfn div dl dt ' +
'em embed ' +
'fieldset figcaption figure footer form ' +
'h1 h2 h3 h4 h5 h6 head header hgroup hr html ' +
'i iframe img input ins ' +
'kbd keygen ' +
'label legend li link ' +
'map mark menu meta meter ' +
'nav noscript ' +
'object ol optgroup option output ' +
'p param pre progress ' +
'q ' +
'rp rt ruby ' +
'samp script section select small source span strong style sub summary sup ' +
'table tbody textarea tfoot th thead time title tr track ' +
'ul ' +
'var video ' +
'wbr'
).split(' ');

var exposesHTMLElement = typeof HTMLElement !== 'undefined' &&
typeof HTMLElement.prototype !== 'undefined',

exposesHTMLUnknownElement = typeof HTMLUnknownElement !== 'undefined' &&
HTMLUnknownElement.prototype !== 'undefined',
el,
isGenericElement,
isUnknownElement,
elementType;

if (!('__proto__' in ({}))) {
document.write('<strong>__proto__ doesn\'t seem to be supported. Tests are not run, but elements are still inspected.</strong>')
}
document.write('<ul>');

for (var i = 0, len = elements.length; i < len; i++) {

el = document.createElement(elements[i]);

if ('__proto__' in el && exposesHTMLElement) {
isGenericElement = el.__proto__ === HTMLElement.prototype;
}
if ('__proto__' in el && exposesHTMLUnknownElement) {
isUnknownElement = el.__proto__ === HTMLUnknownElement.prototype;
}

elementType = isGenericElement ? 'generic' : isUnknownElement ? 'unknown' : '';

document.write(
'<li><span class="label">' + elements[i] +
':</span><span class="value ' + elementType + '">' +
el +
'</span></li>');
}
document.write('</ul>');
})();
</script>

I haven't played with implementing this for anything but details tags, but this should be enough to start.