Three lion cubs

Ankit Agrawal was a Summer 2019 intern on Uber’s Developer Experience team, focused on building developer tools for Android Java engineers. In this article, he offers a bird’s eye view of his internship experience at Uber.

February 12, 2019 was a day of many emotions. I was ecstatic to learn that I would begin an engineering internship at Uber in less than four months, and exhilarated at the thought of working alongside seasoned developers.

Ankit AgrawalHowever, I also experienced fear and uncertainty. Having just completed my freshman year in college and having taken only a few computer science courses, I would face a steep learning curve and have to perform in a production environment–where outcomes truly mattered.

During my summer internship at Uber, tackling cutting edge technical problems first-hand as a part of Uber’s Android/Java Developer Experience team helped me overcome these fears. Developing a feature to surface custom errors inline through an editor provided me with the valuable opportunity to problem-solve, collaborate, and engage in technical discussions with professionals in the field.

My Uber internship gave me new technical skills, showed me what it was like to work on a team in a production environment, and gave me confidence in what I can contribute. This experience will shape how I approach my next year of college and, I assume, my eventual career.

Ramping up

May 28, 2019, the first day of my internship, my nerves began to settle as I entered Uber’s San Francisco office. I saw other interns ranging from just a year or two older than me, to PhD students in the final years of their programs, and I continued to ask myself whether or not I could excel here. However, my initial week was dedicated to intern social events and lectures on Uber’s existing technologies. In the process, I bonded with fellow interns and was introduced to key terms and architectures used at the company. I felt comforted after realizing that we were all in the same boat, with the same goal: to learn and become better engineers.

My sense of belonging continued to grow as I met my team. I had been paired with a mentor and manager for the summer. My mentor would be working closely alongside me, providing advice and input to help guide me as I tackled my summer project; my manager would supervise my work and facilitate cross-team collaboration, ensuring things ran smoothly and addressing any issues brought up among the team. 

During my first week at Uber, my mentor reached out to me, inviting me to lunch with him and the rest of the team throughout the week. I was familiarizing myself with our team atmosphere and our office space, and was eager to collaborate with everyone in the coming weeks.

After my onboarding experience, I immediately immersed myself in the regular work cycle, ready to learn. As a part of the Android/Java Developer Experience team, I got a sense of the wide breadth and importance of the team’s role within the larger Developer Platform and Infrastructure teams. Sitting in on a Developer Platform All-Hands meeting, having team lunches marked by technical and non-technical discussions, and participating in global team syncs and bi-weekly meetings, I came to understand my team’s role: building tooling that expedites developer timelines and improves the programming experience for all software engineers at the company.

Before I could dive into my internship project, however, I had to bring myself up to speed on crucial technologies and frameworks. Terms like Buck and monorepo felt foreign to me, so I reached out to my mentor for any resources he had, and soon enough, I began entering Git and Buck commands without a second thought.

With my tooling knowledge growing, I sat down with my manager and mentor to discuss my summer project: developing a feature to surface custom Buck build errors inline in IntelliJ IDEA. Prior to my internship, the team had already created tooling to allow for the compile-time checking of code in Uber’s Android app codebase when using our Buck build system. Open source static analysis tools like ErrorProne and NullAway had been incorporated to detect additional build errors, and my team leveraged tools like its own Buck Log Analyzer (BLA) to capture such errors effectively. The next step was to actually visualize these errors in the IntelliJ IDEA editor in order to make full use of this pre-existing tooling and allow developers to be more efficient in correcting mistakes in their code. 

Being able to develop a feature that could enhance the developer workflow was empowering, yet my lack of prior experience and fear of failure still troubled me. I was nervous about my potential to achieve our team’s vision for this project. However, after just a few days, I felt confident in my ability to execute on this project with my team’s support and guidance.

Climbing the curve

June 10, 2019, less than three weeks into my internship, I was already intensively researching the company’s Android monorepo and IntelliJ IDEA Community Edition’s codebase. Combing through the vast and expansive layers of code to understand what options exist for parsing and displaying errors was fascinating. I was breaking down the code, analyzing it piece-by-piece, and making connections between files to establish a protocol for collecting and surfacing annotations in a developer’s workspace. Although necessary, the trial-and-error process could prove tedious; constantly hitting roadblocks or producing seemingly non-functional code gave me exposure to lesser celebrated, but very important, elements of engineering.

Still, I valued the experimental nature of our work. I was on a team critical to the software development process at Uber, and I knew that my project would ultimately improve engineering productivity and workflows. By iterating on potential solutions, I could find one that was sustainable, effective, and efficient. After just four weeks, I had achieved significant progress by displaying an annotation in IntelliJ. Having envisioned such a proof of concept to occur much further into my internship, I was delighted to see an initial model upon which I could expand my project.

Terminal window
Figure 1. In the initial version of my project’s annotation style, build errors were annotated with a yellow highlight, blue underline, and yellow sidebar error stripe.

 

With this progress came a growing sense of confidence. The more involved I got in the project, the more comfortable I felt on my team. With time, I began raising questions when I felt blocked without the fear of feeling stupid; engaging in architecture and design discussions with my mentor and manager; updating the team on my project; and handling comments on my diffs with my mentor to enhance my project’s code structure and quality.

What was most invigorating, however, was the opportunity to collaborate with team members globally. Early on, I was introduced to one of Uber’s cultural norms: “We value ideas over hierarchy.” While I had heard the phrase, I was unsure whether or not I would truly be able to contribute to technical conversations with my much more experienced team mates. Fortunately, due to my newly acquired knowledge of BLA, I was able to begin technical discussions and collaborate with engineers on the Amsterdam-based Android/Java Developer Experience team. 

After working with their tool, I felt comfortable voicing my own opinions and suggestions on how to improve BLA to successfully pipe the error outputs I needed, and the Amsterdam engineers were eager to listen. The global team was curious about the functionality of my project, and the engineers were truly invested in enhancing their tools to improve the developer workflow.

Now, I was finally feeling like a valuable member of the team, empowering me to work through the challenges and bring my project together.

Reaching the top

July 18, 2019 proved another day of emotions as I landed a few hundred lines of code in a single day. I took a sigh of relief knowing that my project, the Uber Error Parser, was entering the codebase, and that the project could successfully annotate custom build errors inline in IntelliJ, which is just what my team had hoped for. I had landed code only once before, and to be honest, I did not think much of it at the time. Developers land diffs every day, and this was just one small part of a massive repository of code. I soon realized that the same thousands of engineers adding to this codebase would now be able to see the code I developed, code meant to help them with their everyday tasks by centralizing their workflows and increasing their vigilance in correcting errors in their projects. The feeling was both gratifying and frightening. 

It was rewarding to witness firsthand the fruits of my labor come together. The project felt like an insurmountable behemoth of a task when I first joined Uber. Now, however, I could proudly walk my peers through a clear breakdown of the functionality and components behind my end product, something I did successfully during my final 30-minute presentation for a Developer Platform tech talk.

Diagram of system architecture
Figure 2. The Uber Error Parser listens to changes in the file where custom build errors are printed. The tool transforms these errors to annotate code inline using a RangeHighlighter and MarkupModel.

 

Uber Error Parser architecture

Uber’s BLA was already being utilized across the company to capture and print custom build errors to a specific file whenever a build was run. My project, the Uber Error Parser, uses information these printed errors contain to later access details such as the line number and error message.

Storing build errors

Given that the captured errors are formatted by BLA as JSONObjects, they are collected as such. However, having the JSONObjects is not enough, since annotating errors using just the line number results in the tracking of only a specific line number rather than the erroneous piece of code itself.

As a result, the tool utilizes these JSONObjects to locate their corresponding Program Structure Interface (PSI) Elements. PSI elements are the internal representations of code within a file as interpreted by the IntelliJ Platform. These elements can be used to track the specific section of code with an error, ensuring accurate error tracking. The PSI elements are then stored in an ElementAndString class that contains both the element and the error message associated with it.

Transforming and Visualizing Errors

Once the PSI elements are collected, the errors are displayed in the editor using a MarkupModel and a RangeHighlighter. The MarkupModel stores all the annotations (e.g., code highlighting and text colors, among others) in a given file, with each file maintaining its own MarkupModel. The RangeHighlighter, on the other hand, is a customizable annotation tool that developers can use to display external annotations in a file.

To obtain the proper annotation style, the Uber Error Parser retrieves attributes stored in an AnnotationStyles class, which contains features such as the gutter icon and error stripe type. We want the errors to seamlessly integrate within the overall annotation style of IntelliJ, which is why we chose a regular red, wavy underline with a red error stripe on the sidebar and an error gutter icon. This design closely matches the functionality of other errors and warnings already produced through IntelliJ.

Terminal window
Figure 3. The Uber Error Parser provides build error annotations for developers when working inside the Android monorepo with IntelliJ.

 

The RangeHighlighters for a given file are finally stored in an ArrayList, and those ArrayLists are mapped in a HashMap to their corresponding file name, simplifying what the feature has to track in the project.

Real-time updates

Once a build is run, the tool can visualize the additional build errors that occur. To ensure the annotations are accurate for future builds, we implemented a VirtualFileListener class in the parser to listen for when files are created, changed, or removed. In these cases, the file produced by BLA is checked and the file annotations are updated as necessary.

Iterating on my project

Visualizing the annotations in the editor–signifying a proof of concept for my project–was beyond rewarding. Previously, developers within Uber’s Android monorepo had to check for errors from a separate terminal after running a build, ultimately splitting their workflow, reducing efficiency, and increasing the time required to catch and correct errors. Now, with the Uber Error Parser, developers could be more vigilant about maintaining their code.

While I felt gratified and proud of my achievements, I couldn’t just rest on my laurels. There was more work to be done to ensure that the tool functioned as seamlessly as possible and that I addressed user feedback promptly, making the solution even more useful for Android developers. In the following weeks of my internship, I proactively fixed bugs surrounding the annotations, functionality conflicts with already-existing plugins, PSI element updates, and more. I resolved these issues individually, improving the code quality with time. This process also meant engaging in more hands-on discussions and pair coding with my mentor to determine cleaner ways to write the code while also allowing for the same functionality. As these issues were resolved, I could develop more unit tests with Mockito to ensure optimal code functionality and structure.

Even with these updates, there was still much that can be expanded upon within the project. My college classes had described the iterative nature of software development, but this was the first time I was seeing code I had worked on experience such dynamic changes. For example, I had considered utilizing a FileEditorManagerListener to provide developers with the option to disable annotations once they are no longer relevant (e.g., once lines with custom build error annotations have been edited). With further research, I discovered an alternative method for visualizing annotations by creating and registering an annotator (named ErrorAnnotator) that runs as a background process and annotates PSI elements when necessary (Figure 4). Leveraging this technique resulted in additional features that could ultimately become incorporated into the Uber Error Parser framework for complete error visualization integration into IntelliJ IDEA.

Diagram of system architecture
Figure 4. An alternative architecture for the Uber Error Parser leverages the Annotator interface for more comprehensive error annotations in IntelliJ (e.g., including Gutter icons).

 

Error visualization is just the first part–incorporating an AnnotationStyles class means that developers could input their own error types and attributes to visualize in the IDE. Now, dead code, static analysis information, and code coverage details are just a few of the additional code quality metrics that can be surfaced by potentially utilizing the Uber Error Parser’s baseline framework.

Climbing higher

Looking back on my time at Uber, I am extremely grateful for the opportunities I had on the Android/Java Developer Experience team. The professional and technical growth I achieved through these 12 weeks was beyond what I could have imagined, and something I can take with me as I continue pursuing my undergraduate education. The next time I face a learning curve like this one, though, I can be confident in harnessing the skills I have learned (and maybe have a few more courses under my belt, too).

Comments