Making Debugging Easier: Stacktrace Support in Logpoints



This content originally appeared on DEV Community and was authored by Serah Nderi

After wrapping up my Outreachy Internship and my work with the SpiderMonkey team implementing the Iterator.range among other features, I spent some time contributing to Firefox Developer Tools. My task was to implement a checkbox that allows Developers to print a stacktrace for a logpoint.

If you’re unfamiliar with logpoints, here’s a quick refresher. In the Firefox Debugger, when you click on the gutter next to a line of code, an “Add log” option appears. Clicking it opens an input where you can enter a JavaScript expression.

Photo by Rubaitul Azad on Unsplash

Every time that line is executed, the expression is evaluated and its result is printed to the console — similar to using console.log(), but without modifying your source code. While this is incredibly useful, it can be even more powerful when paired with a stacktrace. By printing the stacktrace along with the result, developers can better understand the context in which a line was executed — helping track down where and why a function was called.

Adding a Stacktrace option would:

  • Add a Show stacktrace option checkbox to the logpoint when enabled.
  • Show stacktrace information — similar to console.trace()
  • Requires changes in both the server component — to retrieve the stacktrace, and the client component for UI updates.

Understanding the Implementation

At first, the implementation seemed straightforward — famous last words. The key tasks were:

  • Locate the files to modify.
  • Add a checkbox UI element labeled “Show stacktrace”.
  • Update component state to track the checkbox state (checked or unchecked).
  • Connect that state to the logic that retrieves and displays the stacktrace.

The end-to-end flow looked like this:

  • The showStacktrace boolean starts from the Conditional Panel UI (the checkbox),
  • Passes through the client-side logic,
  • Gets sent with the setBreakpoint command
  • Is received on the server,
  • And is finally used to conditionally trigger logEvent.

Challenges Faced

1. Manipulating React State

The ConditionalPanel.js is implemented as a class component, utilizing the hyperscript functions (div, input, label, etc.) imported from React DOM factories instead of JSX. This presented a learning curve compared to the functional components and hooks I’m more familiar with.

Instead of using useState hooks, I had to work with class instance properties and understand the component lifecycle methods. This not only introduced complexity in managing state but also made the debugging process more nuanced, especially when ensuring state flowed correctly from the UI to the server logic.

2. Controlled and Uncontrolled elements

Speaking of state, I experienced a lot of React UI desync problems. The Show Stacktrace UI was being rendered based on outdated props or state due to lifecycle render quirks. While the state was changing correctly, the changes weren’t reflected in the DOM until a later re-render.

As a result, I spent quite some time reading GitHub issues on the same topic. While they enhanced my understanding on the topic, most of them were not browser-focused and relied heavily on React hooks, which didn’t apply directly to my case.

However, this particular GitHub issue turned out to be incredibly useful.

The key was using defaultChecked instead of checked, which makes the checkbox uncontrolled. In our case, we realized we didn’t need to store _showStacktrace on the component instance. Instead, we could use:

defaultChecked: this.props.breakpoint?.options?.showStacktrace

This allowed us to simplify the component by avoiding internal state and relying on form submission (FormData.get) to read the final value when needed:

input({
  type: "checkbox",
  id: "showStacktrace",
  name: "showStacktrace",
  defaultChecked: this.props.breakpoint?.options?.showStacktrace,
}),

The checkbox behavior now:

  • Uses defaultChecked instead of checked, so the initial state is set once when the component first renders.
  • There’s no onChange handler to update any component state.
  • The value is read from the form data only when the form is submitted via handleFormSubmit. After the initial render, the parent component has no way to programmatically change the checkbox state. When the form is submitted, the code retrieves the checkbox value like this:
const formData = new FormData(this.panelNode);
const showStacktrace = formData.get("showStacktrace") === "on";

This boolean value is then passed to the saveAndClose function, which forwards it to setBreakpoint, fully replacing the use of this._showStacktrace and aligning better with the form-based approach.

3. Checkbox Disappearance onClick in Firefox

This is because when I clicked on the checkbox, it triggered this onBlur event on the editor input. My mentor Nicholas suggested I add the e?.explicitOriginalTargetcheck to handle Firefox-specific event properties. This change checks if the explicitOriginalTarget (which is Firefox-specific) is within the conditional panel, which should prevent the panel from closing when you click the checkbox.

 onBlur = e => {
   let explicitOriginalTarget = e?.explicitOriginalTarget;
   // The explicit original target can be a text node, in such case retrieve its parent
   // element so we can use `closest` on it.
   if (explicitOriginalTarget && !Element.isInstance(explicitOriginalTarget)) {
     explicitOriginalTarget = explicitOriginalTarget.parentElement;
   }

   if (
     // if there is no event
     // or if the focus is the conditional panel
     // do not close the conditional panel
     !e ||
     (explicitOriginalTarget &&
       explicitOriginalTarget.closest(
         ".conditional-breakpoint-panel-container"
       ))
   ) {
     return;
   }

   this.props.closeConditionalPanel();
 };

Testing the Implementation: Mochitests

After completing the implementation, I needed to verify that everything worked as expected, and make sure that we do get the stacktrace. For Firefox, this means writing Mochitests Mozilla’s testing framework that allows us to simulate user interactions and verify expected behaviors.

From Internship to Contribution

Contributing to Firefox DevTools has felt like a natural next step after my internship, building on the foundations I laid while working on Iterator.range with the SpiderMonkey team. The experience working on Iterator.range provided a solid foundation, but DevTools presented its own unique challenges and opportunities for growth.

Impact for Developers

This small enhancement to logpoints makes a significant difference for developers debugging complex applications:

  • Context Awareness: Stacktraces provide immediate context about how execution reached a particular point
  • Debugging Efficiency: Developers can now get the information they need without switching between tools
  • Optional Verbosity: The checkbox allows developers to control information density based on their current needs.

Contributing to Firefox

  • For anyone interested in contributing to Firefox DevTools, my experience suggests:
  • Start Small: Focus on well-defined features or bug fixes to understand the codebase.
  • Ask Questions: The Firefox development community is supportive and responsive.
  • Test Thoroughly.

I’m grateful for the guidance provided by my mentor Nicholas throughout this process. His feedback and support was instrumental in bringing this feature to completion.

You may also like:
Implementing Iterator.range in SpiderMonkey

Decoding Open Source: Vocabulary I’ve Learned on My Outreachy Journey


This content originally appeared on DEV Community and was authored by Serah Nderi