Debugging a complicated chain of events that led to a bug

We built Jam to help you file bugs. But we’re not perfect! Here’s a quick teardown on one of our own bugs—a bug that affected some users during their first Jam install.

We’re writing this because we’re imperfect, and we think it’s a great chance to show you how crazy bugs can be.

Imagine you’ve just heard about Jam, a free browser extension for reporting bugs quickly, with network requests and console logs automatically included. You’re interested, so you install the extension and log in using Google Oauth.

You go to take your very first screenshot, and… your clicks aren’t registering!

Clicking and dragging isn’t working!

That was the bug: after logging in with an OAuth provider (not email), Jam wouldn’t work on the first page you visited. Refreshing the page fixed this issue, but it feels terrible for a user to hit a bug in their first experience with your product.

How did this happen?!

As with the trickiest of bugs, the buggy code had been unchanged for ages (around four months, on a speedy startup timeline).

One of our engineers had recently refactored our frontend code to only initialize our main React store if a user was authenticated. This was a great change! Now, components that used these stores could guarantee that a user was logged in, instead of having to do a conditional check on every userId access.

This next part is what unearthed the bug.

Along with this change, we needed to listen and initialize our main store when a user logged in. When this would happen, we re-initialized the app. The code looked something like this:

And the problem was actually inside the setupJamUi function.

This is how the bug happened:

  1. When a user logs in, the Oauth flow would take longer to trigger the login reaction than the Email auth flow. So on line 116, a newly-logged-in Oauth user would be missing the tabStore (which held auth data), and call this.init() on line 125, when the auth data propagated in shortly after.
  2. When the init function was called for the second time, the setupJamUi function would be called a second time, to set up our extension’s UI.
  3. This setupJamUi function seemed to handle multiple runs well, it even called removeJamUi() in its first line, before it called createReactRoot() and attached React to the created root. And the createReactRoot function would remove an existing React root as well. Seems great!
  4. Until we hit a function, monkeyPatchDocument. This function was responsible for monkey-patching a dependency of a 3rd-party library that we used for toast notifications. When we tried to use our web toasts inside our extension frontend, they didn’t work, because we rendered our extension’s frontend inside the shadow DOM. This 3rd-party library react-hot-toastwas requiring another library, gooberwhich added and removed its styles to the head. The easiest way around this was to monkey-patch the document variable and document.head property. So we wrote monkeyPatchDocument to do so.
  5. Like the rest of our UI initialization functions, this monkeyPatchDocument function was written thinking it was safe to be run multiple times. But it was not! Instead of only copying the previous <head> tag from the previous fake document, it appended the entire previous fake document into the new one! So anything that was put inside the old fake body would be copied over as well when our intent was only to preserve the existing style data.

What a complicated chain. But we started out with a bug report that told us it was related to Oauth! So how did we solve this?

Debugging

We started with tracing the code. The extension UI is built on top of React and is anchored to a shadow DOM element. When every browser tab loads a page, we inject a script for the Jam UI. This inserts a <jam-ui> tag and mounts shadow DOM underneath it via .attachShadow.

In normal conditions, we see this DOM being attached. The mouse handlers are coordinated by a React hook when the “screenshot capture” component is being rendered.

useMouseDragListeners(rootEl, {
  onMouseMove: ...,
  onMouseDown: ...,
  onMouseUp: ...,
}),

Essentially we are adding event listeners to #__jam-ui-container and after setting breakpoints, we confirmed that the listeners are added but never get triggered. The mystery continues…

After reproducing the issue, we once again examined the DOM and found an anomaly —

We have two instances of our react root element. What?! This is unexpected. This means that we are double injecting or rendering twice which explains why the mouse handlers are not getting triggered.

We continued to trace this all the way down the chain, verifying that each function worked as expected. After fixing this, the DOM was back to normal and the click handlers worked again!

0:00
/

Our takeaway

We hope you enjoyed reading this write-up of one of our own bugs!

While the best ways to prevent bugs have been hotly debated, the reality is that bugs happen. We use tools such as Typescript and eslint, have a dedicated QA, and staging & preview builds, and this bug still managed to impact a customer!

Our philosophy is that bugs will still happen, so giving engineers the data to perform debugging matters even more. Jam lets bug reporters quickly file bugs with network requests, and console logs, and even enables session replays to capture UI issues such as the one that happened here. We hope you’ll enjoy trying it out!

Dealing with bugs is 💩, but not with Jam.

Capture bugs fast, in a format that thousands of developers love.
Get Jam for free