Skip to content

A Beginner's Guide to Writing RStudio IDE Features

sharon wang edited this page Mar 15, 2023 · 29 revisions

Introduction

Learning a new codebase can be a daunting task, and trying to add a feature to a codebase that you're still trying to learn can be more daunting still. The purpose of this article is to help you understand the RStudio IDE codebase a little better and kick start your feature development.

Understanding the Architecture

At this point, you may be familiar with the fact that the RStudio IDE has two deployment models: Desktop and Server. Desktop is basically a simplified version of Server, so we'll start by understanding how Server works, and then come back and look at how that is modified for Desktop.

Server

RStudio Server has a multi-process architecture. It's composed of a single "Server" process and one or more "R Session" processes. The server process is responsible for acting as an HTTP server. The web browser sends HTTP requests and the server responds as any web server would - by supplying the requested resource (if available).

The R session processes, or R sessions, are responsible for executing any R code. R sessions are created on a per-user basis. Each user can have one active session, and the server implements a form of sticky sessions based on the client ID to keep the user connected to their currently active session. Because there are many R sessions, as opposed to one server process, and R sessions are allocated per user most things that need to happen on a per user basis will be done in the R session.

RStudio Server Architecture

This diagram shows process names as they appear on Unix systems (e.g. Ubuntu, macOS, RedHat).

Desktop

As you can see from the diagram below, the RStudio Desktop architecture, while still multi-process, is significantly simpler. The main differences between RStudio Server and RStudio Desktop are the apparent lack of a web browser and the existence of only one R session.

The R session in RStudio Desktop has the same responsibilities as each R session in RStudio Server. Since there can only be one user, there is no need for more R sessions. The "Desktop" process, on the other hand, does have the same responsibilities as the "Server" process, but it also is responsible for acting as the web browser and displaying a GUI.

Starting with RStudio version 2022.12.0+353 (Elsbeth Geranium) this is achieved using Electron.

In older versions, this was achieved using QtWebEngine. See here for more details on QtWebEngine.

RStudio Desktop Architecture

This diagram shows process names as they appear on Windows systems.

The Frontend

So far, everything that's been discussed, except the web browser, we refer to as the backend, and the frontend has been glossed over. In the server architecture section, it was briefly mentioned that the server process is responsible for serving the resources requested by the web browser. These resources (usually a mixture of HTML, CSS, images, and JavaScript) are what we refer to as the frontend.

If you examine the source code of RStudio, you'll find that there are no HTML files. There is also very little JavaScript code. This is because we use a framework called GWT which we use to write Java code that gets transpiled into JavaScript. In turn, the HTML that the server returns is built by the JavaScript code.

There are small amounts of native JavaScript code embedded within the Java code for circumstances in which we need to access raw objects and browser APIs. Generally, however, we prefer to write anything of a reasonable magnitude in Java, due to the advantages that Java offers in terms of type safety, refactoring capabilities, widget libraries, and the ability to design scalable architectures.

The frontend is implemented roughly according to the MVP architecture, with an event loop that receives events from an input source and dispatches them to their handlers.

Before You Begin

Take a look at the other pages in this wiki. Many of them contain useful information that will help you get started. In particular, you should follow through the instructions in RStudio Development to get your development environment set up correctly.

RStudio's code is licensed under the GNU Affero General Public License ("AGPL"). Before you begin, make sure you and your employer (if any) are aware of your responsibilities under the terms of this license, including the responsibility to release any derivative work under the same license. If you further intend to contribute your feature to RStudio for inclusion in a future release, please see our CONTRIBUTING guide for details.

Writing Your Feature

Generally when writing a feature for the RStudio IDE, your work will need to span the frontend and the backend.

Communicating Between the Backend & Frontend

Because your feature probably spans the full stack of the RStudio IDE, you'll probably need a way for the frontend and the backend to talk to each other. Fortunately, so did we. This section covers how to communicate between the frontend and the backend. The later sections, "On the Frontend" and "On the Backend", will discuss in more detail how to make these changes in the relevant locations.

From the Frontend to the Backend

There are two main ways to make a request of the server from the frontend: via a URI endpoint, or via an RPC. Most of the time, you'll probably want to use RPCs to manage your communication. URI endpoints are useful when you want the web browser to be able to treat the backend as a normal web server - e.g. asking for a resource such as CSS or an image.

To add an RPC or a URI endpoint, you'll need to add the relevant handling code on the server, as well as calling code on the client. For a URI endpoint, the calling code on the client may be as simple as adding a link element to the HTML document.

In reality, an RPC is just a special URI endpoint that the server handles differently, but there is a significant amount of scaffolding in place that makes RPCs easier to write.

From the Backend to the Frontend

Occasionally, you may find the need to notify the frontend of a change in the backend. For example, if a user makes a change to their global or project preferences using the RStudio R API, you need to send the updated preferences to the frontend. This can be done by enqueuing an event to be processed by the frontend's event loop.

Caveats

When adding communication between the frontend and backend, try to avoid doing heavy operations during startup. If possible, reduce the scope of the operation occurring at startup to the bare minimum and postpone the heavy operation until absolutely necessary.

Desktop-Specific Communication Channels

In RStudio Desktop, the rstudio executable (rstudio.exe on Windows) and the transpiled GWT code hosted in QtWebEngine can directly communicate with each other.

GWT to RStudio.exe

DesktopFrame.java declares methods, such as setWindowsTitle(), callable from GWT code but implemented in the Qt C++ code of rstudio.exe. These methods are asynchronous and cannot directly return values. A callback pattern can be used if a return value is needed, such as in getDisplayDpi().

DesktopGwtCallback.cpp contains the C++ implementation of these methods for Linux and Windows. On Mac, some methods are implemented in DesktopGwtCallbackMac.mm in order to leverage Objective-C/Cocoa APIs.

Similarly, DesktopMenuCallback.java declares methods such as setCommandEnabled() related to populating and manipulating the Qt-based main menu in rstudio.exe. The C++ implementation lives in DesktopMenuCallback.cpp.

The underlying technique for GWT to C++ communication is based on QWebChannel in Qt5.

RStudio.exe to GWT

The Qt C++ code can invoke functions implemented in DesktopHooks.java, using QtWebEngine's runJavaScript method. These methods show up under window.desktopHooks, and are always asynchronous; see existing usage for examples of how to get the return value. For example, search for isCommandEnabled in DesktopGwtWindows.cpp.

On the Backend

Because the majority of the functionality of the server and desktop processes already exists and is fairly stable, it's most likely that the majority of any backend work required for your feature belongs in the R session process. Most of the interesting new features are tasks which will be done on a per user basis.

Understanding the Code Structure

Before delving into the internals of the codebase, it may be useful to have your project set up. If you haven't already, follow the instructions found here to get the code and set up your workspace.

Now that you have your project set up, you should see some folders that look something like this:

RStudio Backend Project Structure

Each folder corresponds to a component of RStudio. See the table below for descriptions of each component.

Component Description
core This component contains utility classes that are shared amongst multiple components of RStudio.
desktop This component contains code specific to the RStudio Desktop deployment mode. This will be linked with core and to create the rdesktop executable.
diagnostics This component creates a diagnostic application which generates diagnostic reports. See here for more details.
ext This component contains external libraries. You may notice that this looks empty in your Qt Creator project because currently the only library it contains is header only.
monitor This component is responsible for collecting and recording metric information.
r This component is responsible for making calls into R code very easy.
server This is code specific to the RStudio Server deployment mode. This will be linked with core and server_core to make the rserver executable.
server_core This component contains code which can be shared amongst server components.
session This component contains code which will be executed in the R session. It links with core and r to create the rsession executable.

Based on the table above you should hopefully be able to see that your new code probably belongs in the session component.

Most of the time, you should be able to ignore almost all of the subcomponents within the session component except modules. Here is where you will find a module for most major features of the IDE. It is a good idea to take some time to look through some existing modules to get an idea of how the code fits together.

Some examples of features and their corresponding modules:
Git Integration - modules/SessionGit.cpp, modules/SessionGit.hpp
Shiny App Viewer - modules/SessionShinyViewer.cpp, modules/SessionShinyViewer.hpp, modules/SessionShinyViewer.R R Markdown Support - modules/rmarkdown/*

For a more complicated feature, like R Markdown Support, it is necessary to further divide the code. A separate folder can be used to keep the related code together. Also note that it is possible to use both R and C++ to implement your session module, as was done in the case of the Shiny App Viewer.

Getting Started with Your Module

To add a module to RStudio, you will need to create an initialization function for your module. Each module has its own initialization function, so it is necessary to use namespaces to keep them distinct. Here is an example SessionMyModule.hpp:

/*
 * SessionMyModule.hpp
 *
 * Copyright (C) 2023 by Posit Software, PBC.
 *
 * Unless you have received this program directly from Posit Software pursuant
 * to the terms of a commercial license agreement with Posit Software, then
 * this program is licensed to you under the terms of version 3 of the
 * GNU Affero General Public License. This program is distributed WITHOUT
 * ANY EXPRESS OR IMPLIED WARRANTY, INCLUDING THOSE OF NON-INFRINGEMENT,
 * MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE. Please refer to the
 * AGPL (http://www.gnu.org/licenses/agpl-3.0.txt) for more details.
 *
 */

namespace rstudio {
namespace core {
   class Error;
}
}

namespace rstudio {
namespace session {
namespace modules {
namespace my_module {
   core::Error initialize();
}
}
}
}

You may add other things to your header file as you find the need, but keep in mind that it is a good idea to keep your headers as minimal as possible and avoid including other headers wherever possible to avoid polluting translation units. You can find more guidance in the Coding Standards document.

Later you'll want to add things to your initialize function, but for now it should be enough to just return Success() (include from "core/Error.hpp")

Using R in Your Module

Depending on what your feature should do, you may find it useful to write some of your routines in R. Before you start combining R and C++, you may find it useful to read through this section of Hadley Wickham's Advanced R book.

To use R in your module, you should create an R file alongside your module. By convention, we generally name this the same as the related CPP and HPP files (e.g. SessionShinyViewer.hpp, SessionShinyViewer.cpp, and SessionShinyViewer.R).

To help keep the global environment clean, it is best to add your R functions to a special RStudio namespace called tools. This can be done by defining your function using .rs.addFunction. If your function will be an RStudio R API function, you should instead use .rs.addApiFunction. For example, the following block of code will add a function called .rs.myNewFunction to the tools namespace. Alternately, if .rs.addApiFunction had been used, the new function would be called .rs.api.myNewFunction.

.rs.addFunction("myNewFunction", function(<parameters here>){
   # Function body here.
})

When calling R code from your C++ module, it is important to call it "safely" (i.e. no exceptions will be thrown to the top of the stack), because any errors that occur in R could cause the entire R Session process to stop. This is why you will find that RPC methods and other functions used in the R Session follow the much more C-like pattern of returning an Error class rather than throwing an exception.

Helpfully, the RStudio IDE already has some utilities built in to help you with that. You can find most of the helpful functionality in the component called r (See here for more details about components). It's probably a good idea to take a look at some of the functions available in r/RExec.hpp and r/RSexp.hpp before you get started. If you find any of the function definitions confusing, it's always a good idea to use Qt Creator's Find Usages feature, in the right click menu, to look for examples of how the function is used.

Additionally, you will have to register your R file as a source file for your module inside your initialize function.

Below is an example of calling myNewFunction from inside a helper function in your module's cpp file:

/*
 * SessionMyModule.cpp
 *
 * Copyright (C) 2023 by Posit Software, PBC.
 *
 * Unless you have received this program directly from Posit Software pursuant
 * to the terms of a commercial license agreement with Posit Software, then
 * this program is licensed to you under the terms of version 3 of the
 * GNU Affero General Public License. This program is distributed WITHOUT
 * ANY EXPRESS OR IMPLIED WARRANTY, INCLUDING THOSE OF NON-INFRINGEMENT,
 * MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE. Please refer to the
 * AGPL (http://www.gnu.org/licenses/agpl-3.0.txt) for more details.
 *
 */

#include "MyModule.hpp"

// This is used with an ExecBlock for registering your R source as well as
// other parts of your module, which will show up later in the guide.
#include <boost/bind.hpp>

// The rstudio::core::Error class is used for all error handling, and is
// preferred over throwing exceptions.
#include <core/Error.hpp>
// This includes the ExecBlock class, mentioned above.
#include <core/Exec.hpp>

// This provides the registration functions for registering parts of your
// module.
#include <session/SessionModuleContext.hpp>

// These are both used for executing R and manipulating R objects.
#include <r/RExec.hpp>
#include <r/RSexp.hpp>

using namespace rstudio::core;

namespace rstudio {
namespace session {
namespace modules {
namespace myModule {

namespace {

void aHelperFunc()
{
   // Create an RFunction object and tell it to look for your function by name.
   r::exec::RFunction myNewFunction(".rs.myNewFunction");

   // Do this as many times as necessary to set all the parameters.
   myNewFunction.addParam(<param name>, <param value>);

   // This is used to protect raw R objects (SEXPs) on the stack.
   // When its destructor runs, all R objects protected by this object
   // will then be unprotected and hence eligible for garbage collection.
   //
   // All of the RStudio APIs returning raw R objects will also require you
   // to pass a Protect object, to ensure that all raw objects are protected
   // from the garbage collector (as almost all R APIs can trigger a garbage
   // collection)
   r::sexp::Protect protect;

   // This will hold the return value of your function. If you don't have a
   // return value, or you don't care about it, you can omit this as well as
   // "protect". A Protect object will still be used, but the parameter-less
   // "call" function will create one for you.
   SEXP result;
   Error error = myNewFunction.call(&result, &protect);

   // Errors can be treated like booleans. True if there's an error. False if
   // there was no error.
   if (error)
      // Handle your error as you see fit.

   // Otherwise do some stuff with the result.
}

} // anonymous namespace

Error initialize()
{
   using boost::bind;

   // This exec block collects all the registrations we need to do and executes
   // them all at once at the end, returning an error if any fail.
   ExecBlock initBlock;
   initBlock.addFunctions()
      (bind(sourceModuleRFile, "MyModule.R"));

   return initBlock.execute();
}

} // namespace myModule
} // namespace modules
} // namespace session
} // namespace rstudio

Using C/C++ in Your R Code

You may find it necessary to call back into your C++ code from R. This can be done by creating a helper function that returns a SEXP (shorthand for a pointer to an S-expression) and takes SEXPs as parameters. Then, you can register it to your module by adding RS_REGISTER_CALL_METHOD(<method name>); to your initialize function. This macro is included from r/RRoutines.hpp.

By convention, we prefix all C++ functions that are intended to be called from R with rs_.

To call the function from your R code, simply use result <- .Call("<method name>", <param1>, <param2>, ..., PACKAGE = "(embedding)"). Setting PACKAGE = "(embedding)" ensures that you will load the correct C++, and safely avoid any naming collisions.

r/RSexp.hpp provides many helpful functions for manipulating SEXP objects, including safely converting your parameters to C++ objects and easily converting your return value from a C++ object into a SEXP.

In my example aHelperFunc has a return type of void, and there's no clear handling of the error that may get returned by the call to myNewFunction. Most likely, the proper handling of an error will be to simply return it to the client, and many of your helper functions will likely return Error.

Adding an RPC

Now that you know how to implement the guts of your feature, you probably need a way to get whatever it does back to the user. The most common way to do that is via an RPC. For now, we'll just consider how to add the RPC to the server and we'll look at how to call the RPC from the client in the On the Frontend section.

When writing your RPC try to consider the flow of execution: the user initiates an action, the client needs the server to do all or some of the work for the action, the client sends an RPC request to the server, your module's RPC implementation is invoked, the response is returned to the client.

All RPCs have the following signature:

Error myRpcFunction(const json::JsonRpcRequest& request,
                          json::JsonRpcResponse* pResponse)

json::JsonRpcRequest and json::JsonRpcResponse are in the namespace rstudio::core, but if you recall from the earlier example, we've added the using namespace rstudio::core; declaration near the top of the cpp file. They can be included from <core/json/JsonRpc.hpp>.

These functions, like your other helper functions, should be implemented in the unnamed namespace block. This helps to keep your module isolated and avoid cluttering other namespaces unnecessarily. To make the implementation available to the server's routing code, we register it with the server in a similar way to the source R file. Here is the updated initialize function after adding myRpcFunction:

Error initialize()
{
   using boost::bind;

   // This exec block collects all the registrations we need to do and executes
   // them all at once at the end, returning an error if any fail.
   ExecBlock initBlock;
   initBlock.addFunctions()
      (bind(sourceModuleRFile, "MyModule.R"))
      (bind(registerRpcMethod, "my_rpc_function", myRpcFunction));

   return initBlock.execute();
}

Now your RPC should be available to call from the client.

On the Frontend

Before you begin, you should set up the project (rstudio/src/gwt) under a Java IDE of your choice.

Where to Add Your New Code

Navigating a new codebase can be challenging, and full understanding is something that will build up slowly over time. Deciding how to modify a codebase that you're unfamiliar with can be a stumbling block, but try to remember that moving files around is easy compared to actually implementing the feature. Take your best guess based on other examples that you see, and if there's something not quite right about it can be addressed during code review.

With that being said, here's a quick overview of the project structure on the frontend:

  • src
    • com - this is where we keep the GWT jars
    • org.rstudio - this is where all the RStudio implementation goes. Generally speaking, all of your work will be somewhere under this folder.
      • core - this is where general purpose utilities go. For example, if you wanted to add a new type of widget, say a special type of button that could be used when creating a UI pane, that would go here.
      • studio.client - this is where everything more specific goes. For example, if you wanted to add a new UI preferences pane for some new preferences that were added, that would go under here.

After that, where your work goes will be quite specific to the work that you are doing. The best way is probably to look at a similar feature and follow the same pattern that it does.

Generally speaking, we try to keep the frontend code organized in an MVP fashion, with models in a model folder and UI elements in a views folder.

How to Call an RPC

Access from the client to the server is provided via RemoteServer. If you take a look at that file, you'll see that it has quite a lot of functionality in it, and that it extends from a large number of interfaces. This architecture keeps the all the server code together, but if used directly could be a bit unwieldy. Fortunately, the frontend codebase uses GWT dependency injection (GIN). The interfaces that RemoteServer extends allow us to isolate the functionality into much smaller and more manageable components. Then, in the constructor of the class you can add a parameter for the relevant interface and the annotation @Inject above the constructor, and GIN will inject the singleton instance of RemoteServer into the constructor. This way, objects only need to know about the parts of RemoteServer that are directly relevant to them.

Now suppose that you want to call the my_rpc_function that was defined in an earlier example from the client. Here's what that would look like:

/*
 * MyModuleOperations.java
 *
 * Copyright (C) 2023 by Posit Software, PBC.
 *
 * Unless you have received this program directly from Posit Software pursuant
 * to the terms of a commercial license agreement with Posit Software, then
 * this program is licensed to you under the terms of version 3 of the
 * GNU Affero General Public License. This program is distributed WITHOUT
 * ANY EXPRESS OR IMPLIED WARRANTY, INCLUDING THOSE OF NON-INFRINGEMENT,
 * MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE. Please refer to the
 * AGPL (http://www.gnu.org/licenses/agpl-3.0.txt) for more details.
 *
 */

package org.rstudio.studio.client.<appropriate.pkg.location>.model;

import org.rstudio.studio.client.server.ServerRequestCallback;
import org.rstudio.studio.client.server.Void;

public interface MyModuleServerOperations
{
    void myRpcFunction(ServerRequestCallback<Void> request);
}

This example assumes that my_rpc_function takes no parameters and returns nothing. If your request returns a value, you can change the Void parameter in the ServerRequestCallback definition to an appropriate type. If your function takes parameters, you can add parameters to your function of an appropriate type after the request parameter. There are a lot of existing examples in the client code if you do need to do this.

After you've created your interface, you need to have RemoteServer extend it; however, a direct extension may not be the right way. There are some ServerOperations interfaces which group other ServerOperations interfaces into logical groups. For example, if your feature is related to a specific part of the workbench, it would make more sense to have WorkbenchServerOperations extend from your interface. RemoteServer already indirectly extends from WorkbenchServerOperations, so after you've changed that you can go ahead and implement your interface's function on the RemoteServer class. Even if your feature is an isolated feature, it should still be grouped under the Server interface.

Here's an example of implementing your feature in RemoteServer.java:

...

public class RemoteServer extends Server
{

...

    @Override
    public void myRpcFunction(ServerRequestCallback<Void> callback)
    {
        sendRequest(RPC_SCOPE, MY_RPC_FUNCTION, new JSONArray(), callback);
    }

...

    // session methods
    ...
    private static final String MY_RPC_FUNCTION = "my_rpc_function";
    ...
}

Then you just need to call myRpcFunction from the class that is called by the UI. For example:

/*
 * MyModule.java
 *
 * Copyright (C) 2023 by Posit Software, PBC.
 *
 * Unless you have received this program directly from Posit Software pursuant
 * to the terms of a commercial license agreement with Posit Software, then
 * this program is licensed to you under the terms of version 3 of the
 * GNU Affero General Public License. This program is distributed WITHOUT
 * ANY EXPRESS OR IMPLIED WARRANTY, INCLUDING THOSE OF NON-INFRINGEMENT,
 * MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE. Please refer to the
 * AGPL (http://www.gnu.org/licenses/agpl-3.0.txt) for more details.
 *
 */

package org.rstudio.studio.client.<appropriate.pkg.location>.model;

import com.google.inject.Inject;

import org.rstudio.studio.client.server.VoidServerRequestCallback;
import org.rstudio.studio.client.server.Void;
import org.rstudio.studio.client.<appropriate.pkg.location>.model.MyModuleServerOperations;

public class MyModule
{
    @Inject
    public MyModule(MyModuleServerOperations myModuleServerOperations)
    {
        myModuleServerOperations_ = myModuleServerOperations;
    }

    public void callMyRpcFunction()
    {
        myModuleServerOperations_.myRpcFunction(new VoidServerRequestCallback()
        {
            @Override
            public void onSuccess()
            {
                // Handle a successful RPC call.
            }

            @Override
            public void onFailure()
            {
                // Handle a failed RPC call.
            }
        });
    }

    private MyModuleServerOperations myModuleServerOperations_;
}

Depending on the complexity of your feature, this could be a UI class directly (and should be in a ui package instead of model package), or your structure could include an events package as well as module and ui. The Plumber module uses all three, so it might be a good thing to read through to get an idea of how you want to structure your own feature on the frontend.

Testing Your Feature

Now that you've written your feature, you'll want to make sure that it works the way you intended. There are two main ways to test changes to the RStudio IDE right now: unit tests, and ad-hoc tests.

Unit Testing

Unit tests are not required for a PR to be accepted, but they are extremely helpful. Besides helping to ensure that future changes don't break the intended functionality of your feature, unit tests can also help other developers understand the expected output of the components of your feature. Similarly, they can also highlight valid vs. invalid input and other edge cases that might not be immediately clear.

To get started with unit testing on the front end, try reading through GWT Unit Tests. For unit testing on the backend, take a look at CPP Unit Tests.

Ad-Hoc Testing

When ad-hoc testing your feature, make sure to try things that should cause error or warning messages to pop up to the user, rather than just focusing on the "working" workflow. It's just as important that a user gets an informative error message when they do something out of order as it is that the feature works when they do everything right.

There are a couple of other considerations to keep in mind when you're doing your testing: platforms and deployment method. RStudio Server is supported on Linux and RStudio Desktop is supported on Linux, Mac OS, and Windows. For an official list of supported platforms, see the download list here. Desktop and Server may have different behaviors, especially if you've changed something in the backend, so it is a good idea to test your feature in both configurations. If your feature involves significant changes, it is also highly recommended that you test your feature on at least one distribution of all the supported platforms (e.g. Mac OSX 10.13, Windows 8, and Ubuntu 16).

Fixing Your Feature

If something isn't working properly, the first step is to figure out whether the backend is broken, the frontend is broken, or both. After that it should be easier to figure out exactly what isn't working and then fix it.

Generally speaking, debugging the RStudio Server is easier than debugging RStudio Desktop so if you find a bug in RStudio Desktop the first step should be to determine whether you can reproduce it in RStudio Server. If you can, it's best to proceed with debugging process using RStudio Server.

Frontend or Backend?

Before you get into debugging, there are a few things to look for that might help determine whether the issue is occurring in the frontend or the backend.

Look at the Browser Development Tools

Anywhere on the IDE, right-click to bring up the context menu and choose Inspect or Inspect Element. This should bring up the browser's development tools. There are two major things to check for here: an error on the Console tab, or a non-200 response on the Network tab. You may have to reproduce the error while the development tools are open to see any errors.

If you're seeing a non-200 response from an HTTP request, it is more likely that the bug is in the backend. If you're seeing a JavaScript error (such as an object is undefined), it's more likely that the bug is in the frontend. Ultimately, determining where the bug is located is very contextual, and it wouldn't be uncommon to guess wrong and have to switch where you're debugging.

Look at the Logs

If you've run rserver-dev or rdesktop-dev from the terminal, you should be able to see logs directly on the terminal. Reading these can help you get an idea of where the bug is.

If you're running a version of RStudio that was installed via a package (such as a .deb or .rpm), you can find log files by opening the Help > Diagnostics > Show Log Files menu item.

Look at the RPC Request Log

The RPC Request Log can be opened via the menu (Help > Diagnostics > Request Log) or using hot key ctrl+`. You should be able to see a record of all the RPC requests made by the frontend and the responses sent back by the backend. Here's an example of what the RPC Request Log might look like:

RCP Request Log

To see the details of the request and the response, just click on the response in the list. To exit this view, press escape.

Add More Logging

If you are still having trouble determining where the bug is happening, it can be beneficial to add logging on both the client and server. This can help you see where you are getting unexpected values and show where to start debugging. Because you can add logging to both the frontend and backend at the same time and only need to reproduce the issue to generate the logs, this can be a faster method of narrowing down the source of the issue than just jumping straight into debugging.

Which Backend Process?

If you've determined that the issue is in the backend, there is an additional step: determining which backend process contains the bug. Whether you're running RStudio Server or RStudio Desktop, there are effectively two processes: rserver (or rdesktop), and rsession. There will always be exactly one rserver (or rdesktop) process. When running RStudio Server, there may be multiple rsession processes. For more information, see the "Understanding the Architecture" section. Try to use similar methods as described in the "Frontend or Backend?" section to determine which process should be debugged.

After you know which backend process you want to debug, you'll need to attach and start debugging. If the process is the rsession, it will be easier to find the right process if you only have one rsession running. If you do have multiple running, you can find the PID of the desired rsession by running Sys.getpid() from the R console of the same rsession.

If you need to debug something that happens during the startup of the rsession process, you can either stop all existing rsession processes and then use a debugger that allows you to attach to an unstarted process (Qt Creator has this feature via gdb), or add a short sleep at the very beginning of rsession startup to give you time to attach.

The Problem is in My R Code

If you've used R in your session module, and the R code is the source of the issue, you may find that you are unable to hit breakpoints set inside your .rs.addFunction declarations. This is because breakpoints work by effectively modifying the function to include a call to browser() and RStudio does not recognize the .rs.addFunction syntax as a function declaration. The easiest way to debug your .rs. function is to call debugonce(.rs.myFunc) from the R console and then trigger the error. This article provides more information about setting breakpoints in R and RStudio.

How do I Debug the Frontend?

To debug the frontend code, you'll need to run ant in SuperDevMode, which can be done by running ant devmode (or ant desktop for RStudio Desktop, but again, I would highly recommend you use RStudio Server for debugging issues unless they only occur in RStudio Desktop) from the src/gwt folder under your RStudio git repository root.

To start, open the browser's development tools by right-clicking to open the context menu and selecting Inspect or Inspect Element. Then switch to the Sources tab. The easiest way to get to the location where you would like to set a breakpoint is to open the file search with cmd+o or ctrl+o (hot keys may vary depending on platform and browser) and then start typing the name of the file.

Alternatively, if you have access to an IDE that supports debugging web-based frontends, such as IntelliJ, you can use that instead of the browser's development tools. You may need to configure the IDE to correctly find the SuperDevMode server and the source maps, and it's possible you may need to do some extra work to get the IDE to work correctly with GWT.

Preparing Your Feature for PR

Once you're satisfied that your feature is complete and working correctly you're almost ready to submit a PR for your branch. Before you do, it's a good idea to review your code, clean it up, and craft a PR comment that will help any reviewers understand the intent of your feature.

Cleaning Up Your Code

There are a couple of things you can do to clean up your code that will help make your PR process smoother. A quick review of your code can help you find and remove leftover debugging code, spelling mistakes, and any cruft. Try not to leave commented out code - if the code isn't necessary for the feature it's better to remove it completely. If possible, try to match the style of your code to the style of the existing code.

Additionally, comments aren't required by our coding standard but they can be useful to help reviewers understand what's going on in the code. Comments can also be extremely useful for maintaining the code later on when it hasn't been touched in a while, and everyone's forgotten what it's supposed to do. It's recommended to add explanatory comments to any part of your code that is counterintuitive or moderately complex.

Writing a Good PR Comment

A good PR comment should include a full description of the changes that your branch includes. Besides the main functionality or bug fix on your branch, you should also include a description of any other minor features or bug fixes that you made on the same branch (although it's best to keep the scope of a branch as small as possible).

If your PR is a feature, we recommend including an explanation of how to use all aspects of the feature, including step-by-step instructions where possible. If your PR involves UI changes, it is useful to include screenshots of what the new UI looks like. If your PR is fixing a bug or closes a feature request, including a phrase like Closes #123 or Fixes #123, where #123 is the Git Issue being closed by the PR. Additionally, including an explanation of what was causing the bug and how it was fixed is very helpful.

Choosing Reviewers

If you're not sure who you should include as a reviewer for your PR, you can take a look at the history of the file(s) (or folder(s)) that are affected by your PR to see who has been modifying that area of the codebase recently. Don't stress too much about picking the right person. If you assign someone who doesn't feel they have the requisite familiarity with that part of the codebase to review your PR, they'll probably help you find the right person.

Continued Learning

In addition to this article and the other pages in this wiki, the following are some useful documentation that can help you as you continue to learn about the RStudio IDE codebase.

Clone this wiki locally