10  Debuggers

Published

2024-12-20

Warning

The contents for section are under development. Thank you for your patience.

Bugs can cause our app to crash, produce incorrect results or displays, and result in other unexpected behaviors. Fixing bugs in our app-package is an important step to ensure our application continues behaving as expected. Using an interactive debugger can help us find the root cause of the error.

I’ve created the shinypak R package in an effort to make each section accessible and easy to follow. Install shinypak using pak (or remotes):

install.packages('pak')
pak::pak('mjfrigaard/shinypak')
library(shinypak)

List the apps in this chapter:

list_apps(regex = '10')

Launch apps with launch()

launch(app = '10_debugger')

Download apps with get_app()

get_app(app = '10_debugger')

Debugging in RStudio is covered elsewhere,1 so we’ll focus on debugging our Shiny app code using Positron’s interactive debugger.

At the time of this writing, the 2025.01.0-39 pre-release of Positron was available for testing.

10.1 Debugging with browser()

Interactive debugging (e.g., using browser() or setting a breakpoint) allows us to ‘peek inside’ a function’s scope to view intermediate values/variables and break down the execution line by line.

%%{init: {'theme': 'base', 'themeVariables': { 'fontFamily': 'Inconsolata'}}}%%
flowchart LR
 subgraph Function["Function Scope"]
        Running["Function executes"] --> Browser("<code>browser()</code>")
        Browser --> Debugger("View variables/values")
    subgraph Browse["Interactive Debugger"]
        Debugger --> Step["Execute line by line"]
    end
    Step --> Exit("Exit <code>browser()</code>")
    Exit --> Exec["Execution resumes"]
  end
    
    style Debugger fill:#4CB7DB,stroke:none,rx:10,ry:10
    style Exit fill:#4CBB9D,stroke:none,rx:10,ry:10
    style Step fill:#4CB7DB,stroke:none,rx:10,ry:10
    style Running fill:none,stroke:none,rx:10,ry:10
    style Browse fill:#F8F8FA,stroke:#333,stroke-width:1px,rx:5,ry:5
    style Browser fill:#4CBB9D,stroke:none,rx:10,ry:10
    style Function fill:#E5E6EB
    style Exec fill:none,stroke:none,rx:10,ry:10


Unfortunately, using browser() and breakpoints are not as straightforward within reactive contexts, because browser() can disrupt the reactive flow, making maintaining an interactive debugging session challenging.

10.1.1 Reactive browsing

In a Shiny context, we want to pause execution without altering or stopping the reactive flow. Fortunately, Shiny already has a function that performs this: observe().

Within a Shiny server() function, any call to observe() creates a reactive observer that ‘listens’ for changes to reactive dependencies (and runs any enclosed code whenever those dependencies change).

%%{init: {'theme': 'base', 'themeVariables': { 'fontFamily': 'Inconsolata'}}}%%
flowchart TD
subgraph Function["Server Scope"]
    Fun["Reactive flow"] --> Obs["<code>observe()</code>"]
    subgraph Observer["Observer Scope"]
        Obs --> Browser["<code>browser()</code>"]
        subgraph Debugger["Interactive Debugger"]
        Browser -- Invalidates Observer --> View["View variables/reactives"]
        View --> ExitBrowser["Exit <code>browser()</code>"]
        end
        ExitBrowser --> ExitObserver["Exit <code>observe()</code>"]
    end
    Fun <--> ExitObserver
end

style Function fill:#E5E6EB
style Fun fill:none,stroke:#E5E6EB,stroke-width:3px,rx:10,ry:10
style Debugger fill:#F8F8FA,stroke:none,rx:5,ry:5
style Observer fill:#FFFFFF,stroke:#4CBB9D,stroke-width:3px,rx:10,ry:10
style Browser fill:#4CB7DB,stroke:none,rx:3,ry:3
style View fill:#4CB7DB,stroke:none,rx:3,ry:3
style ExitBrowser fill:#4CB7DB,stroke:none,rx:3,ry:3
style Obs fill:#FFFFFF,stroke:#4CBB9D,stroke-width:3px,rx:10,ry:10
style ExitObserver fill:#FFFFFF,stroke:#4CBB9D,stroke-width:3px,rx:10,ry:10

When browser() is called from within the observe() scope, the code execution pauses and temporarily suspends the reactive flow so we can inspect the environment (without altering or stopping the reactive flow).

Interactive Debugger & Shiny Apps

Placing a call to browser() inside observe() will trigger the interactive debugger when the observer is invalidated.

observe({
  browser()
})

This allows us to interactively examine variables and reactive expressions (within the scope of the observe() function).

10.2 Example: ggplot2movies app

In the Resources we developed an application using the ggplot2movies data.2 In the following sections we’re going to use the interactive debugger to see the inner workings of the ‘Remove missing’ checkbox.

Remove missing checkbox in ggplot2movies development application (click to enlarge)

Remove missing checkbox in ggplot2movies development application (click to enlarge)

We’ll begin by placing the observe() and browser() breakpoint inside the module server function containing the ‘Remove missing’ input (right after the moduleServer() function). We’ll close the observe() scope after the reactive inputs() are created:

show/hide debugging functions in the module server function
dev_mod_scatter_server <- function(id, var_inputs) {
  moduleServer(id, function(input, output, session) {
    observe({
      browser()
      
      all_data <- fst::read_fst("tidy_movies.fst")

      graph_data <- reactive({
        if (input$missing) {
          tidyr::drop_na(data = all_data)
        } else {
          all_data
        }
      }) |>
        bindEvent(input$missing)

      inputs <- reactive({
        plot_title <- tools::toTitleCase(var_inputs()$plot_title)
        list(
          x = var_inputs()$x,
          y = var_inputs()$y,
          z = var_inputs()$z,
          alpha = var_inputs()$alpha,
          size = var_inputs()$size,
          plot_title = plot_title
        )
      })
    })
    
    observe({
      output$scatterplot <- renderPlot({
        plot <- sap::scatter_plot(
          df = graph_data(),
          x_var = inputs()$x,
          y_var = inputs()$y,
          col_var = inputs()$z,
          alpha_var = inputs()$alpha,
          size_var = inputs()$size
        )
        plot +
          ggplot2::labs(
            title = inputs()$plot_title,
            x = stringr::str_replace_all(
              tools::toTitleCase(inputs()$x), "_", " "),
            y = stringr::str_replace_all(
              tools::toTitleCase(inputs()$y), "_", " ")
          ) +
          ggplot2::theme_minimal() +
          ggplot2::theme(legend.position = "bottom")
      })
    }) |>
      bindEvent(graph_data(), inputs())
    
  })
}
1
Observer scope
2
Call to browser() (execution paused)
3
Read tidy_movies.fst data
4
Missing data checkbox logic
5
Reactive values from user inputs
6
Module graph code (outside of observe() scope)

Then we’ll load the changes:


Ctrl/Cmd + Shift + L

And launch the app using:

ggp2_launch_app(options = list(test.mode = TRUE))

Launch app with the shinypak package:

launch('10_debugger')


Don’t forget to load any debugging calls with devtools::load_all() before re-launching the app!

devtools::load_all('.')

Or

Ctrl/Cmd + Shift + L

As noted above, browser() pauses code execution and activates the interactive debugger mode, allowing us to view objects, execute code, and ‘step through’ the function line-by-line.

10.2.1 IDE Changes

When the app is launched, Positron alerts us that we’re in debugging mode by making a few changes to the IDE:

  1. The Run and Debug sidebar menu item is displayed and the footer is highlighted in blue3

Positron IDE in debugger mode (click to enlarge)

Positron IDE in debugger mode (click to enlarge)
  1. The dev_mod_scatter.R file in the Editor highlights the call to browser() in yellow

Debugger in file Editor (click to enlarge)

Debugger in file Editor (click to enlarge)
  1. The Console displays the interactive debugger prompt: Browse[1]>

Debugger in Console (click to enlarge)

Debugger in Console (click to enlarge)

observe() does not inherently pause or interrupt other reactive processes—it just triggers when changes occur within its scope. So when we’re using observe() for debugging, we need to define the context (or scope) for its behavior.

The output in the Console (shown above) tells us the tidy_movies.fst data are downloaded, but our placement suspends the execution of the application before these data are loaded and the graph is rendered in the UI (shown below).

Suspended ggplot2movies data app (click to enlarge)

Suspended ggplot2movies data app (click to enlarge)

The interactive debugger can only access variables and values inside the observe() scope, but this process can be incredibly useful for addressing bugs (and for exploring how an application works). In the next sections, we’ll ‘step through’ the module function to explore how the missing values are removed from the graph.

10.2.2 Variables and values

We want to use the interactive debugger to proceed through the module function until the data object enters the logic for the missing checkbox, and then we can confirm its structure.

Step through/execute each line of code by entering n in the Console.

Browse[1]> n

As we ‘step through’ the function, Positron’s Console displays the debug at location, followed by the code line number:

Full path and line number to the file containing our call to browser() (click to enlarge)

Full path and line number to the file containing our call to browser() (click to enlarge)

In the Editor, the same line of code is highlighted in yellow:

Corresponding line number in Editor (click to enlarge)

Corresponding line number in Editor (click to enlarge)

The line creating graph_data gives us a hint for how the missing data are removed (i.e., with bindEvent()), but we’ll explore this more in Print debugging.

Under Locals in the DEBUG VARIABLES sidebar, we can see all_data is listed as a <data.frame>, and graph_data are listed as a <reactive.event>:

Click to enlarge DEBUG VARIABLES in sidebar

Click to enlarge DEBUG VARIABLES in sidebar

10.2.3 Inspecting variables

We can use the Console to evaluate code while the interactive debugger is running. This comes in handy if we want to check the structure of an object inside a module (like all_data).

Browse[1]> str(all_data)
'data.frame':   58788 obs. of  10 variables:
$ title      : chr  "$" "$1000 a Touchdown" ...
$ year       : int  1971 1939 1941 1996 1975 ...
$ length     : int  121 71 7 70 71 91 93 25 97 ...
$ budget     : int  NA NA NA NA NA NA NA NA NA ...
$ rating     : num  6.4 6 8.2 8.2 3.4 4.3 5.3 ...
$ votes      : int  348 20 5 6 17 45 200 24 18 51 ...
$ mpaa       : Factor w/ 5 levels "G","PG","PG-13" ...
$ genre_count: int  2 1 2 1 0 1 2 2 1 0 ...
$ genres     : chr  "Comedy, Drama" "Comedy" ...
$ genre      : Factor w/ 8 levels "Action": 6 3 6 ...

This gives us an idea of the total rows before missing are removed.

10.2.4 Inspecting values

The reactive values and inputs can also be viewed in the Console, and we can see graph_data() is ‘bound’ to input$missing with bindEvent(). We can confirm the input$missing value in the Console:

Browse[1]> input$missing
[1] TRUE

This tells us the ‘Remove missing’ checkbox has been selected, and we can verify the missing values have been removed from graph_data():

Browse[1]> identical(
              nrow(tidyr::drop_na(all_data)), 
              nrow(graph_data())
            )
[1] TRUE

Using browser() to ‘step through’ an application gives us a better understanding of the ‘order of execution’, and it lets us see how input$missing and bindEvent() work together to remove the missing values with the checkbox.

Recap

Recap: Interactive Debuggers

During regular development, an interactive debugger can let us inspect variables and execute the code line-by-line. In Shiny functions, the debugger lets us track the execution of reactive expressions and observers, which allows us to unravel reactivity-related issues that are often difficult to diagnose.

browser() and observe() are powerful tools for debugging our application. Navigating a function using the interactive debugger gives us control over the execution of each line.

Please open an issue on GitHub


  1. For an introduction to the IDE’s debugging tools, see Debugging with the RStudio IDE. Debugging is also covered in Advanced R, 2ed and Mastering Shiny.↩︎

  2. You can refresh your memory on the ggplot2movies application in Section 9.3.↩︎

  3. Previous versions of Positron highlighted the footer in red.↩︎