Let’s just imagine what it’s like for a major e-commerce website in the weeks leading up to the Christmas holiday shopping rush: they’ve horizontally scaled their server, prepared a proper marketing campaign, and calculated their revenue projection.
It takes a lot of planning to pull everything off without a hitch and keep customers happy. Clearly, the holiday season can be hectic for shoppers and businesses alike.
The e-commerce website’s executives can’t help but feel confident leading up to the holiday shopping rush. All of this planning had led them to anticipate a torrent of orders that will multiply their profit margin. They even have to refrain themselves from opening up a bottle of champagne and celebrating too early!
However, they eventually notice that something is off: numerous user visits, for some unknown reason, end at the category page. Why are potential customers leaving the site?
The technical staff decide to figure out what’s going on by opening an arbitrary category page and scrolling down. They notice that the page scrolls to the top by itself at some point between 1 and 10 seconds, preventing the user from browsing and selecting products. It looks like they’ve found the smoking gun.
Some users may tolerate such inconveniences once or twice, but if they try to navigate other categories on the website and encounter the same issue, they will simply go to a better-functioning website in search of their desired product.
This is a disaster waiting to happen for any business - especially when they’re anticipating peak user traffic, and they risk losing major profits.
At this point you’re probably wondering: how to avoid such an issue? Let’s examine one of the many use-cases of why and how you may need to debug a getter/setter in JavaScript.
Assume we have N user groups and each group has the same probability of `p` that a user will close the website because of a pesky layout issue (for example, like a problem with scrolling or botched animation).
That probability depends on the number of critical UX issues across all the UI-specific issues (the less critical issues we have, the lower that probability).
Let’s denote `o` as an average order total for a user across all the groups (an average across average total for a user per each group).
For the sake of simplicity, assume at least U users from all groups are placing an order every `t` minute.
Lastly, denote the time for debugging and fixing the issue as `T` (in minutes).
Imagine that `p` = 0.3, `o` = $50, `U` = 50 and `t` = 5.
If we assume the fix will take 300 minutes (5 hours), then we have the following estimate:
```loss = U * p * o * T / t = 50 * 0.3 * 50 * 300 / 5 = $45 000```
If our website is huge, then 1 000 users placing orders every 5 minutes during peak season can easily transform our initial estimate from 45 000 to 900 000.
In the following sections, we’ll demonstrate how that potential revenue loss could and should be reduced to the minimum with the most effective debugging strategies using browser developer tools.
Please note the algorithm of calculating an approximate revenue loss outside the holiday season is the same, although the exact values depend on a selected time frame.
Given the urgency of the issue, engineers must act fast. Trying to reproduce the issue on a local machine leads to nothing.
The best hypothesis we have is that something resets scrollTop
for document.documentElement
at some point, but how to identify what exactly?
A quick search using Chrome dev tools on the production website shows more than 100 places where scrollTop
could be assigned a new value. The following solutions come to mind before we roll up our sleeves and do some detective work:
scrollTop
might get a new value and hope we catch the problem sooner than later.scrollTop
is a configurable accessor property, we can get its descriptor from Element.prototype
. We can patch it with a new setter that uses debugger
as the first statement and then delegates the job of setting the new value to the old setter.Looking at the former option and assuming it will take 60 seconds on average to check every case, we’ll spend 6 000 seconds which is roughly 1.67 hours. In circumstances of high customer load during the holiday season, that is too slow, and the business aims to resolve the issue as soon as possible.
Let’s now look at the second solution, which looks as follows:
(function() {
const scrollTopDescriptor = Object.getOwnPropertyDescriptor(Element.prototype, 'scrollTop');
const originalScrollTopSetter = scrollTopDescriptor.set;
scrollTopDescriptor.set = function(...args) {
debugger;
return originalScrollTopSetter.apply(this, args);
};
Object.defineProperty(Element.prototype, 'scrollTop', scrollTopDescriptor);
})();
All we have to do is to execute this snippet when the category page starts loading, then scroll down and wait for the trap to work. Sure enough, we get a long-awaited breakpoint stop triggered by the debugger
statement in our custom setter.
Looking at the stack trace on the right panel in Chrome dev tools, we quickly find out that the issue relates to the custom marketing scripts.
The intent was to scroll a page to a particular banner, however, a trivial calculation error resulted in 0 assigned to documentElement.scrollTop
. Since that script loads only in production, that explains why the issue wasn't reproducible on a local machine.
We notify the marketing team about the issue, and they promptly deliver a fix, but there is still one question left unanswered.
Is it possible to improve the second solution and make it more elegant?
Chrome dev tools allow us to put breakpoints in multiple scenarios, for example: animations, DOM mutations, conditional breakpoints, XHR/Fetch, event listeners, exceptions.
Most importantly for us, any function could be automatically paused upon execution with the help of dev tools’ debug function without modifying a single line of a source code or manually placing a breakpoint.
Recall that this function takes a function you'd like to debug as an argument and automatically pauses its execution upon invocation. Therefore, it’s possible to rewrite our solution as:
(function() {
const scrollTopDescriptor = Object.getOwnPropertyDescriptor(Element.prototype, 'scrollTop');
debug(scrollTopDescriptor.set);
})();
One may think it's not a big deal if we cut a few lines of code, but the debug
function is much more powerful. Remember that we mentioned scrollTop
is a configurable accessor property?
If it wouldn't be configurable, our previous solution with overriding a property descriptor would fail since one can't redefine a non-configurable object property.
debug
works differently and can wrap a setter (or a getter) from a property's descriptor regardless if it's configurable or not.
Here's an example:
(function() {
const person = {};
Object.defineProperty(person, 'name', {
configurable: false,
get() {
return this._name ?? 'Unknown';
},
set(value) {
this._name = value;
}
});
const nameDescriptor = Object.getOwnPropertyDescriptor(
person,
'name'
);
debug(nameDescriptor.set);
person.name = 'John';
})();
You can try the above-mentioned debugging strategies on this demo page, which is a simplified version of a real project.
The page will scroll up after a randomly picked time in the interval from 3 to 7 seconds when:
Here’s what a stack trace looks like when the page pauses its execution upon scrolling to the top:
Debugging unobvious things like setters and getters of accessor properties in browser developer tools can be a daunting task, especially within the conditions of high urgency and website traffic. When it comes to investigating issues that are beyond your control, it's better to be prepared in advance. Arm yourself with the best strategies and tools for the task if you wish to reduce potential revenue losses.
Here's a quick comparison table of the JS debugging solutions outlined in this article.
Debugging solution | Speed | Preference |
Put a breakpoint on every line where a scrollTop might be assigned a value |
O(n) |
Low |
Override a scrollTop property on Element.prototype with a decorated set method that pauses execution when scrollTop is being assigned a new value |
O(1) |
Medium |
Pass set from scrollTop descriptor as an argument to debug function |
O(1) |
High |