10 Debuggers
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. This chapter will cover how to use an interactive debugger to help us find the root cause of bugs and errors.
TLDR: 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).
Debugging in RStudio is covered elsewhere,1 so we’ll focus on debugging our Shiny app code using Positron’s interactive debugger.
10.1 Interactive debugging
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.
Unfortunately, using browser()
and breakpoints are not as straightforward within reactive contexts. browser()
interacts with the R interpreter by temporarily suspending execution and redirecting the input to evaluate R expressions in the context of the function or script that called it.
Shiny’s asynchronous execution makes it difficult for browser()
to pause the flow of execution, making maintaining an interactive debugging session challenging.
10.1.1 Reactive browsing
In a Shiny context, code inside the server function is executed asynchronously in response to user inputs or reactive dependencies. Shiny does not directly expose the control flow to the user, but we want to pause this 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).
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).
10.2 Example: ggplot2movies
app
In the Resources chapter we developed a slight variation of our app with 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.
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
<- function(id, var_inputs) {
dev_mod_scatter_server moduleServer(id, function(input, output, session) {
observe({
browser()
<- fst::read_fst("tidy_movies.fst")
all_data
<- reactive({
graph_data if (input$missing) {
::drop_na(data = all_data)
tidyrelse {
}
all_data
}|>
}) bindEvent(input$missing)
<- reactive({
inputs <- tools::toTitleCase(var_inputs()$plot_title)
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({
$scatterplot <- renderPlot({
output<- sap::scatter_plot(
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 ::labs(
ggplot2title = inputs()$plot_title,
x = stringr::str_replace_all(
::toTitleCase(inputs()$x), "_", " "),
toolsy = stringr::str_replace_all(
::toTitleCase(inputs()$y), "_", " ")
tools+
) ::theme_minimal() +
ggplot2::theme(legend.position = "bottom")
ggplot2
})|>
}) 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:
launch_app(app = "ggp2")
Launch app with the shinypak
package:
launch('10_debugger')
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:3
Editor
The dev_mod_scatter.R
file in the Editor highlights the browser()
function in yellow
Console
The Console tells us browser()
was ‘Called from: observe()
’ and displays the interactive debugger prompt:
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 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.
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:
In the Editor, the same line of code is highlighted in yellow:
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>
:
In the next section, we’ll explore these variables (and the reactive inputs).
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
).
1]> str(all_data) Browse[
'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:
1]> input$missing Browse[
[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()
:
1]> identical(
Browse[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
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.↩︎
You can refresh your memory on the
ggplot2movies
application in Section 9.2.↩︎Watch this video to learn more about Positron.↩︎
Previous versions of Positron highlighted the footer in red.↩︎