Here’s the thing: most people know it’s better if you test a new system of pipes for a house in a controlled environment before tearing down walls. I think we all agree, right?
So, this was in our minds as we considered how to address the fact that people weren’t able to debug client-side logic visually. And, our product is all about being visual. You know that story; you’ve read it before. My team and I had to come up with a solution that would debug mobile apps on both the client and the server-side. We had to evaluate systems, mechanisms, and approaches without destroying our entire platform. So let me tell you about some of the things we had to consider to ensure a bulletproof full-stack visual debugger.
Playing Around With the Protocol
Right from the start, during the project design, we thought about using the Chrome DevTools Protocol to interact with the application running on the browser. This protocol notifies us when a breakpoint is hit or, in our case, when the execution is paused by the debugger statement.
We started with a small proof-of-concept to test the viability of this approach. This way, we could iterate, fail fast if it came to that, and try something else, all before we even started to modify our product. Plus, we would not have to deal with all the dependencies and details that a big and mature product has.
But you know, first evaluate the piping system, right? So, at this stage, since we were mainly focusing on validating the technology and not on the developer experience, we created a simple .NET application that was able to interact with the browser and complete the basic operations needed for debugging an application.
With the goal of reducing the maintenance cost of this new feature and avoiding broken behavior or deprecated methods in the future, we decided to use the stable version of the protocol (v1.2).
Adding a New Piece to a Big and Well-Oiled Machine
With the approach validated, the next step was to design how to include this new feature in our development environment.
We faced two big challenges. First, the mobile debugger should have a similar experience to an existing feature that already existed in Service Studio: the debugger of server-side logic, even though its implementation would be different.
Second, we wanted our debugger to support all major browsers but, at the same time, we needed to deliver this feature fast to address user pain. They were used to building applications visually, and the existing implementation of the debugger was anything but visual. To accomplish this, we decided to support only the Chrome browser, because the protocol was from Google, so it would be easier to integrate and faster to deliver value. However, we wanted the architecture to be flexible enough so that anyone could add support for new browsers in the future, so we implemented a plugin.
Understanding the Architecture of the Debugger for Server-Logic
Given that the server-side debugger was a piece of software that was a few years old, we could’ve easily cast it away, thus creating two separate things. However, we embraced the challenge instead.
So before tearing down any walls, we had to understand the existing debugger architecture. We got together with a developer who was part of the original server logic debugger team to understand the technology underneath, along with the abstract and functional concepts that should be consistent with the new mobile debugger.
With the API well-defined, we could then refactor all the common logic and reuse it in the new debugger.
We then realized that we had to determine how to do the refactoring (a process that could take some time) needed to achieve our goal while still continuously releasing the product. We had two options:
- Develop in a separate, isolated branch and then merge back to the master branch.
- Put a mechanism in place to guarantee that the code, despite being in the product, is never executed.
We ruled out the separate branch because of some painful past experiences, and we went with the mechanism option.
To isolate the new code, we resorted to using preprocessor directives to guarantee that the code wasn’t even compiled. This approach also helped us migrate the legacy logic in phases. Using the directives, we were able to switch from the old implementation to the new one as it was being migrated. So, by simply defining a new symbol (NEW_DEBUGGER) using #define, we could switch between both implementations, and run tests to validate that everything kept working.
This approach worked so well that we had other teams using the debugger in their daily work without even noticing a thing!
Keeping a Seamless and Awesome Experience
From day one, we did usability tests on paper for multiple scenarios and solutions. This included thinking of any edge cases that could happen when interacting with an external tool. In this project, we had to communicate with Chrome to be able to interact with the running application, so we needed to be prepared for different scenarios, such as when the user, willingly or not, closes the browser mid-session or when there is an error in the application’s runtime.
We then committed ourselves to an experience that was as consistent as possible. The goal was for users not to notice that there was a new debugger.
Following the same approach for debugging server-side logic, which consisted of having specific handles in the generated code where the execution could stop (the handleBreakpoint method), we quickly got the debugger working on client-side logic:
The challenge now was to allow the developer to jump between both client and server-side logic seamlessly. There were some places where this transition could happen, like stepping through client-side code that called server-side actions or jumping between actions on the running thread’s stack.
We adapted the application runtime so that this information could pass on every request made to the server. Now it was up to the development environment to decide where it should execute the commands to evaluate variables or other user commands. With the abstraction created earlier and a common interface defined for both implementations, it was really easy to implement this experience.
With this innovation, developers can easily and seamlessly debug server and client-side logic at once.
The best part of all? When we heard, during usability tests:
"This is just like the server debugger that I’m already used to; it simply works."
Troubleshooting Mindset Right from the Start
Our experience with past projects taught us that when working on a new feature, troubleshooting mechanisms are usually postponed for later stages of implementation. This becomes a problem because of the history of the feature’s development.
Troubleshooting, therefore, was not something we put off; we were thinking about it from the very start. Therefore, since we were creating logs at the same time as we were coding, it was easier to determine which information was most important to log.
Later on, when our alpha testers were already using the feature, we found ourselves resorting to the logs to help them overcome problems. It was a big help to quickly understand which steps led to the problem and come up with the solution.
Another great aspect of having logging information right from the start is that you can use beta programs to validate its effectiveness and guarantee that, when the feature is released, you’ll be much faster at providing support to your users.
Ensuring the Same Experience in the Diverse Mobile World
At some point during this project, we had to detour mid-way to support debugging mobile applications on mobile devices. This new requirement created two additional challenges: How do we provide the same experience for both Android and iOS devices? And how do we reuse all the logic already developed to interact with a browser so it works on a mobile device?
Addressing the latter challenge in Android was easy because the ADB tool provided by Google already allows interaction with WebViews from applications that use the same protocol as Chrome. However, we had to dig throughout the internet to figure out how. Eventually, we stumbled upon a small section on an npm package that had the two commands we needed, listing the WebViews currently running:
adb shell grep -a webview_devtools_remote /proc/net/unix
And connecting to a WebView:
adb forward tcp:9222 localabstract:webview_devtools_remote_<pid>
Testing was next, so it was time for another PoC to determine feasibility. All we needed now was a way to interact with the ADB tool. Luckily, unlike finding the commands to be called, it was way easier to find a package to do it: the SharpAdbClient (link).
For iOS, we had to do more research to find a way to connect to applications that run on Apple devices using a Windows PC, as this is the platform supported by our development environment. But, we were able to find the iOS WebKit Debug Proxy, a project developed by Google, to map the DevTools protocol to Apple’s WebKit.
Given that the proxy was already mapping to the protocol we were using, as soon as we knew how it worked, everything fell into place, and all the code developed so far was reused to work with iOS devices.
Meanwhile, version 11 of iOS was set for release to the general public around the same time we were going to deliver the mobile debugger. No pressure, right? Given the rate of adoption of iOS versions, we wanted to ensure that our debugger was fully compatible by the time of its release, so we started using Beta versions for our validations.This allowed us to figure out some minor details that weren’t handled by the proxy. For example, for the execution to be stopped when the debugger statement is executed, an additional method needed to be executed, the the ‘setBreakpointsActive.’
After working out the details for getting both operating systems ready and because both were working with the same protocol, we were able to ensure the same experience as soon as the session started, regardless of the device being used. Only the device configuration setup was different.
Delivering a Feature in a Released Product
Because of our commitment to component compatibility in the same major version, a top priority was to deliver this awesome feature in OutSystems 10 without causing any problems to existing customers.
Out platform has two components: the development environment and the platform server, and they have separate release cycles. Therefore we implemented a critical, small protocol between them to allow the development environment to discover which version is running on the platform server to adapt and avoid using unimplemented methods.
This versioned API between both components serves as a constant reminder to keep interactions simple and small for the most independence possible.
Additionally, our alpha users needed to be able to test and offer opinions about the debugger throughout the project. So, we used a mechanism of feature toggles to ensure that the code would only execute for our alpha users, while keeping the rest of the world using the old, stable debugger.
Debugging Visually: Brave New World
“With the Full-Stack Visual Debugger, what would usually take 3 hours now takes 5 minutes, and our users now have answers to their questions.” - Sara Gonçalves
This challenging yet mesmerizing project ended up being one of the most satisfactory ever. Debugging time was significantly reduced, and it was finally visual!
OutSystems users now have a seamless experience when debugging both server and client-side logic. Along the way, we extended a set of functionalities, such as debugging when offline, debugging synchronization logic and logic that uses native plugins.
It’s a mad world out there, but we’re ready for it. One implementation at a time.