Testing Javascript bug fixes in production environment

Overview

This is an elaborate post on how I go about testing a Javascript bug fix directly in production environment. I believe the method that I am about to explain is very useful in that it enables one to quickly test a fix without going through below cycle of:

  1. Reproduce the bug in local.
  2. Fix the bug.
  3. Test the bug fix in local.
  4. Deploy to environment for verification.

The technique that I am going to enlist below is possible mainly due to one of the features enabled by JavaScript language called “monkey patching” - the ability to change the definition of a function while or after browser parsing of the function.

Setup

We need a sample HTML/Javascript project with a deliberate “bug” introduced so that we can see how to dynamically patch a fix and test if it works or not. Note that the technique covered here should work on all modern browsers but specifically it is more useful for IE6+. Even though modern browsers have improved the tools required to effectively debug UI apps I still think the below techniques are complementary to them rather than a replacement.

Debug

The below are the few heuristics that I follow to reverse-engineer the root cause for an UI issue. I am assuming that you are thrown into the deep end to fix an issue with neither having access to code base nor any knowledge/memory about the functionality. If not then still the heuristic is helpful.

Ok, here it goes using the above sample project as a bug:

  1. Follow the steps to recreate. (In the above project, the page must display a circle but only a semi-circle is visible.)

  2. Once you had recreated the bug, check if the issue is browser specific. If not browser specific, you can use any browser to debug further. Else, use the browser specifically causing the bug. (In the above project, it is happening in more than one browser so I choose chrome to debug it but it can be any browser. At work, I would have chosen IE.)

  3. This is the important part: Isolate the element that is not behaving as expected a.k.a. the “bug”. (In the above project, the svg element is not behaving as proper.)

  4. Open developer tool (in IE press F12. For other browsers, check their documentation on how to open its developer toolbar).

  5. To search the code in js, first start with the element and check if it is using any id or class. If yes, then search the Javascript file for that id or class or element name. Else if you don’t find anything on the element, check if there are any onload events declared. Or check the css file that is styling the element and search the js files that are loaded after or before the css file to limit the number of js files that you need to look at to debug further. (In the above project, I found the onload event and added a breakpoint there. I could have also searched for element name “svg” because there was only one such element.)

  6. Now we need to find out the code that is the precursor of the “bug”. That is, the code which executed correctly just before the “bug” happened. (In the above project, it is the onload handler)

  7. After adding the breakpoint via dev tool at line #2, I ran the page again and this time the page stopped at the breakpoint location line #2. From here I need to slowly step through the code to understand where it is breaking. (In the above project, I guess it is breaking at line #9 where the d3 js is creating the circle)

  8. If you find the precursor but still are not able to add breakpoint because the js is dynamically generated, then find the js file that is injecting the dynamic script. This can be found via network tab or the html source that has the list of js files that it loads. Then put a breakpoint just before and after this dynamic creation of script. After the script is created, it will appear in the sources window and you can now go and add additional breakpoints that you need. Sometimes (esp. I noticed this in IE) even if you add a breakpoint in the generated script, the browser may not stop at that line. Check the “Fix” section on how to handle this thornier case.

  9. Enable cache (temporarily while you are testing) so that when you reload the page again, the dynamic scripts are retrieved from cache with your added breakpoint intact.

Fix

There are only two techniques you need to test your fix directly in the developer tools/console.

A. While the browser is waiting at the breakpoint line #2, open console in dev tools and paste the below code and click run or press enter. This code effectively redefines the drawCircle() function to the bug-fix version. Now go and run the debugger to completion.

function drawCircle() {
 var container = d3.select("body")
		.append("svg")
		.attr("width", 500)
		.attr("height", 500);
 container.append("circle")
  .attr("cx", 20)
  .attr("cy", 20)
  //note the change from original 30 to 20
  .attr("r", 20)
  .style("fill", "black");
}

In my case, this fix worked and I am done. For complex cases, you might be doing multiple iterations on the fix before you finally conclude that it works in all cases.

How cool is that!

B. Now, for the corner case when your breakpoint may not be working esp. for dynamically injected scripts, when the browser stops at the breakpoint just after dynamic script injection call, you can redefine the function inside the dynamic script with the same function definition except at the first line inside the definition add the “debugger;” statement.

function drawCircle() {
 /* 
Yes, this is the single most important debugging aid that almost nobody is aware of and the best part is this works on all the browsers (<https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/debugger>).
*/
 debugger; // debugger statement

 var container = d3.select("body")
		.append("svg")
		.attr("width", 500)
		.attr("height", 500);
 container.append("circle")
		.attr("cx", 20)
		.attr("cy", 20)
		// note the change from original 30 to 20
		.attr("r", 20)
		.style("fill", "black");
}

Then you need to manually invoke the “bootstrap” function of the dynamic script via console. (In the above example, you need to call “drawCircle()” via console)

drawCircle();

This will stop the browser at this line and then you can add more breakpoints or continue to step-into/step-over via the debug bar.

Caveat1: Remove “debugger;” statement before you check-in the fix. It should only be used as a debugging aid.

Caveat2: Note that this dynamic injection of a function redefinition will not work if the browser already started executing within the function that you are redefining. So always put the first breakpoint just one call stack below the function that you are going to redefine.

Note: In order not to repeat step B every time the browser dynamically injects the script while refreshing the page, you can enable caching via dev tool.

Conclusion

This has turned into a longer article than I anticipated but I hope it helps someone to effectively debug their frontend apps. The method captured above are browser agnostic and has personally helped me save many person hours in debugging and effectively testing the fixes.
Happy debugging!