Ga naar de inhoud

Shiny Source Code Explained: Shiny Options and Developer Mode

With Shiny options, you can control some of Shiny’s behaviour. And there are many options to choose from: think about setting the host and port, enabling trace options and activating developer mode. What do all these options do, and how is it handled behind the scenes? In this edition of “Shiny Source Code Explained” you will find out!

First, you’ll get to know the various available Shiny options, and how to set and get them. Once those basics are covered, it will get more technical: we’ll discuss two different examples in Shiny’s source code to understand how Shiny options change app behaviour, and we’ll find out how the so-called developer mode sets a group of options. There’s plenty to cover, so let’s get started!

Table of Contents

Available Shiny options

First things first: let’s list all the options that are, at the time of writing, available in Shiny:

  • šŸ’» autoreload: continually monitor files for changes, and reload all connected Shiny sessions when a change occurs.Ā Default: FALSE.
  • autoreload.pattern: file pattern that Shiny will monitor.Ā Default: NULL.
  • autoreload.interval: polling interval to monitor changes. Default: 500ms.
  • autoload.r: source R/ folder of a shiny automatically. Default: TRUE.
  • deprecation.messages: Print messages for deprecated functions in Shiny.Ā Default: NULL.
  • šŸ’» devmode: enable Shiny Developer Mode (experimental). Default: NULL.
  • client_devmode: client-side devmode features, in particular the client-side error console (shiny 1.8.1). Very niche usage, and intended for use in Shinylive.Ā 
  • devmode.verbose: also very niche usage. Displays messages printedĀ about which options are being set.
  • error: call a function when an error occurs- for example, this can be browser() or recover().Ā Default: NULL.
  • šŸ’» fullstacktrace: dump “pretty” (FALSE) or full stack traces (TRUE) to the console when errors occur.Ā Default: FALSE.
  • trace: print messages between the R server and the web browser client to the R console.Ā Default: FALSE.
  • host: IP address Shiny should listen on.Ā Default: 127.0.0.1.
  • json.digits: number of digits to use when converting numbers to JSON format to send to the client.Ā Default: I(16).
  • launch.browser: control the default behaviour when an app is run, normally TRUE and will launch the default browser.Ā Default: interactive().
  • mathjax.url: MathJax helps to display mathematical equations on web pages, and this is the URL that loads MathJax. Default: https://mathjax.rstudio.com/latest/MathJax.js.
  • mathjax.config: the querystring used to load MathJax. Default: config=TeX-AMS-MML_HTMLorMML.
  • maxRequestSize: specifies the maximum web request size, which serves as a size limit for file uploads. Default: 5MB.
  • šŸ’» minified: whether or not to include Shiny’s JavaScript as a minified (shiny.min.js) or un-minified (shiny.js) file.Ā Default: TRUE.
  • port: TCP port number, but not any of these: 3659, 4045, 5060, 5061, 6000, 6566, 6665:6669, 6697. Default: random open port.
  • reactlog: enable logging of reactive events, which can be viewed later with the reactlogShow() function. Default: FALSE.Ā 
  • sanitize.errors: whether to show a simple generic error message or not- when deployed this is normally set to TRUE.Ā Default: FALSE.
  • stacktraceoffset: printed stack traces will display srcrefs one line above their usual location.Ā Default: TRUE.
  • suppressMissingContextError: don’t throw an error when invoking a reactive outside of a reactive context- handy for demos. Default: FALSE.
  • testmode: enable various features for testing- used in shinytest2 for example. Default: FALSE.Ā 
  • jquery.version: major version of jQuery to use, currently only values of 3 or 1 are supported.Ā Default: 3.
  • usecairo: allow PNG to render via the Cairo package.Ā Default: TRUE.
  • useragg: allow PNG to render via the ragg package.Ā Default: TRUE.
  • snapshotsortc: How snapshot keys will be sorted- shinytest2 will always sort using the C locale. Default: FALSE.

Ā 

You can find more information in the Shiny documentation.

Get or set Shiny options

There are two methods when it comes to getting or setting Shiny options: using base R, or using functionality from the Shiny package.

Let’s start with the good old options() function from base R. It’s pretty simple, and you can simply set an option like so: options(shiny.maxRequestSize = 10 * 1024^2). This specific option will increase the file size limit to 10MB for file uploads (the default is 5MB). As you can see, you prefix the option with "shiny.". If you want to get all current (Shiny) options, you can call options() without any arguments. You need to look out for options starting with "shiny." for the Shiny specific ones. Do you know what you are looking for? getOption() is your friend. If the option isn’t found, it will return a (chosen) default value.

The second method to get or set Shiny options comes from functions in the Shiny package itself: shinyOptions() to set a value, and getShinyOption() to retrieve a value. Sounds cool, but what’s the difference with using options() from base R?

The main difference is in scopingĀ and where it is used. The options() function sets options globally, for the duration of the R process. The shinyOptions() function sets the value of a shiny option, but unlike options(), it is not always global in scope; the options may be scoped globally, to an application, or to a user session in an application, depending on where it is called. As a Shiny developer, you can currently use it for specifying a caching object that will be used by renderCachedPlot(). For example, you can use shinyOptions(cache = cachem::cache_disk("./myapp-cache")). If you specify this at the top of app.R, this sets the application-scoped cache to be a disk cache that can be shared among multiple concurrent R processes, and persists on disk across reboots. Handy! So is this all it does? Yes and no. For now, you can only set the cache option like in the mentioned example. But you will see shinyOptions() primarily being used internally, so in Shiny’s source code. We’ll come back to that later.

If I may take a guess, the shinyOptions() function is being prepared for the future, and the idea is that you can use it for more options. It’s a nice feature to be able to control scoping of your options- so I definitely see the charm of it.

Developer mode

The developer mode was mentioned earlier, so what does it do? In the options list above, you will see a laptop icon šŸ’» in front of some options. The Shiny team has made things easier for the hardcore Shiny developers with the so-called developer mode. If you enable this developer mode with the devmode option (options(shiny.devmode = TRUE)) or by calling devmode(TRUE), Shiny will automatically enable these options for you.

Shiny Source Code

Right, you now understand what options are available and how you can set them to alter Shiny’s behaviour. We can now get to the real question: how is it handled in the source code?

Let’s take a look at two examples:

  • "shiny.sanitize.errors"
  • options in the runApp() function, specifically "shiny.port"

Example 1: shiny.sanitize.errors

For "shiny.sanitize.errors", the relevant code can be found in middleware.R, and looks like this:

				
					        call = .httpServer(
          function (req) {
            hybrid_chain(
              hybrid_chain(
                withCallingHandlers(withLogErrors(handlers$invoke(req)),
                  error = function(cond) {
                    sanitizeErrors <- getOption('shiny.sanitize.errors', FALSE)
                    if (inherits(cond, 'shiny.custom.error') || !sanitizeErrors) {
                      stop(cond$message, call. = FALSE)
                    } else {
                      stop(paste("An error has occurred. Check your logs or",
                                 "contact the app author for clarification."),
                           call. = FALSE)
                    }
                  }
                ),
                catch = function(err) {
                  httpResponse(status = 500L,
                    content_type = "text/html; charset=UTF-8",
                    content = as.character(htmltools::htmlTemplate(
                      system_file("template", "error.html", package = "shiny"),
                      message = conditionMessage(err)
                    ))
                  )
                }
              ),
              function(resp) {
                maybeInjectAutoreload(resp)
              }
            )
          },
          loadSharedSecret()
        )
				
			

The code in middleware.R basically determines what the Shiny web server does with HTTP requests. The core functionality is wrapped inside .httpServer(), which listens for incoming HTTP requests (req) and executes a function to generate an appropriate response. The appropriate response is determined by so-called handlers. Handlers are functions that automatically respond to certain situations, like errors or successful events. You can think of them as “if this happens, do that” rules.

In the code above, the error handler catches errors when they happen and decides how to respond. The catch handler is a backup that catches any remaining errors and turns them into a proper HTTP response (a 500 error page).

So back to our "shiny.sanitize.errors" option. If an error occurs, it is caught by the error handler. This handler checks if the error is a "shiny.custom.error" or if sanitization is disabled (aka set to FALSE, via the"shiny.sanitize.errors"option, which is retrieved by the getOption() function from base R). If either condition is met, the original error message is returned; otherwise, a generic, sanitized, error message is shown. This makes sure your users don’t see any sensitive information.

Example 2: the runApp() function

We can find other examples in the runApp() function. This function deals with several Shiny options, like "shiny.port", "shiny.launch.browser", "shiny.host" and "shiny.test.mode":

				
					runApp <- function(appDir=getwd(),
                   port=getOption('shiny.port'),
                   launch.browser = getOption('shiny.launch.browser', interactive()),
                   host=getOption('shiny.host', '127.0.0.1'),
                   workerId="", quiet=FALSE,
                   display.mode=c("auto", "normal", "showcase"),
                   test.mode=getOption('shiny.testmode', FALSE)) {
                   ...
                   }
				
			

Again, you see that getOption() is being used to retrieve the option if it was set, and provides a default value otherwise. If you take a closer at the port option, "shiny.port", you will see that it defaults to NULL when it’s not set. If it is NULL, the following happens:

				
					  # ============================================================================
  # Server port
  # ============================================================================
  # determine port if we need to
  if (is.null(port)) {

    # Try up to 20 random ports. If we don't succeed just plow ahead
    # with the final value we tried, and let the "real" startServer
    # somewhere down the line fail and throw the error to the user.
    #
    # If we (think we) succeed, save the value as .globals$lastPort,
    # and try that first next time the user wants a random port.

    for (i in 1:20) {
      if (!is.null(.globals$lastPort)) {
        port <- .globals$lastPort
        .globals$lastPort <- NULL
      }
      else {
        # Try up to 20 random ports
        while (TRUE) {
          port <- p_randomInt(3000, 8000)
          # Reject ports in this range that are considered unsafe by Chrome
          # http://superuser.com/questions/188058/which-ports-are-considered-unsafe-on-chrome
          # https://github.com/rstudio/shiny/issues/1784
          # https://chromium.googlesource.com/chromium/src.git/+/refs/heads/main/net/base/port_util.cc
          if (!port %in% c(3659, 4045, 5060, 5061, 6000, 6566, 6665:6669, 6697)) {
            break
          }
        }
      }

      # Test port to see if we can use it
      tmp <- try(startServer(host, port, list()), silent=TRUE)
      if (!inherits(tmp, 'try-error')) {
        stopServer(tmp)
        .globals$lastPort <- port
        break
      }
    }
  }
				
			

If the port is NULL, Shiny enters a process to find an available port. It first checks if there is a previously successful port stored in .globals$lastPort. If such a port exists, it uses that and then clears the stored value (it will be added back later). Otherwise, it selects a random port between 3000 and 8000, ensuring that it does not pick any ports that are considered unsafe.

Once a port is selected, a temporary server using startServer(host, port, list()) is started. If starting the server does not result in an error, it immediately stops this temporary server with stopServer(tmp), saves this port in .globals$lastPort for future attempts, and exits the loop. This was a success! However, if the chosen port fails, Shiny repeats the process up to 20 times with different random ports. If all attempts fail, it simply moves forward with the last attempted port, allowing a later part of the source code to handle the failure properly.

What about shinyOptions()?

In various places, you will also see the earlier mentioned shinyOptions() function. As mentioned, options set via this function are primarily used internally.

One example can be found when it comes to bookmarking. Bookmarking in an app saves the current state like inputs, selections, or pages. Handy if your user wants to go back to a certain state. To enable bookmarking in a Shiny app, you use the enableBookmarking() function. This function takes an argument (store, which can be "url" ,Ā "server" or "disable") to specify the bookmarking method. Under the hood, it calls shinyOptions(bookmarkStore = store), which sets a global option that tells Shiny how to handle bookmarking:

				
					enableBookmarking <- function(store = c("url", "server", "disable")) {
  store <- match.arg(store)
  shinyOptions(bookmarkStore = store)
}
				
			

Whenever Shiny needs to know about the the type of bookmarking, it will get the bookmarkStore value with getShinyOption():

				
					 store <- getShinyOption("bookmarkStore", default = "disable")
				
			

Very similar to how it’s done with base R, with one little difference: different possibilities for scoping. Getting into how bookmarking exactly works is beyond the scope of this edition of “Shiny Source Code” explained, but perhaps it will end up in a future edition ;).

Activating developer mode

You can activate developer mode with devmode(), which looks like this:

				
					devmode <- function(
  devmode = getOption("shiny.devmode", TRUE),
  verbose = getOption("shiny.devmode.verbose", TRUE)
) {
  options(
    shiny.devmode = devmode,
    shiny.devmode.verbose = verbose
  )
}
				
			

So where in the code does Shiny use this"shiny.devmode" option? Well, that was a bit an adventure to find out, but this happens in a wrapper function called get_devmode_option():

				
					get_devmode_option <- function(
  name,
  default = NULL,
  devmode_default = missing_arg(),
  devmode_message = missing_arg()
) {
  getOption(
    name,
    local({
      if (!in_devmode()) {
        # typical case
        return(default)
      }

      info <- registered_devmode_options$get(name)
      if (is.null(info)) {
        # Not registered,
        # Warn and return default value
        rlang::warn(
          message = paste0(
            "`get_devmode_option(name)` could not find `name = \"", name, "\"`. ",
            "Returning `default` value"
          )
        )
        return(default)
      }

      # display message
      devmode_inform(
        maybe_missing(
          # use provided `devmode_message` value
          devmode_message,
          # If `devmode_message` is missing, display registered `devmode_message`
          default = info$devmode_message
        )
      )

      # return value
      maybe_missing(
        # use provided `devmode_default` value
        devmode_default,
        # if `devmode_default` is missing, provide registered `devmode_default`
        default = info$devmode_default
      )
    })
  )
}
				
			

The first thing the above function does, is checking whether or not devmode is enabled. The function responsible for that is in_devmode(). The in_devmode() function checks for the "shiny.devmode" option:Ā 

				
					in_devmode <- function() {
  isTRUE(getOption("shiny.devmode", FALSE)) &&
    # !testthat::is_testing()
    !identical(Sys.getenv("TESTTHAT"), "true")
}
				
			

So there we have it: our "shiny.devmode" option! Part one of our mystery. The second part is about the underlying options of the developer mode, so we need to take a closer look at the above wrapper function get_devmode_option().

When developer mode is off, and in_devmode() returns FALSE, get_devmode_option() works just like getOption(name, default), returning a default value if the option isn’t set. But when developer mode is on, it checks if the option is registered in registered_devmode_options. If it’s not, it warns the user and falls back to the default. If it is registered, it can return a different value specifically meant for developer mode (devmode_default) and display a message (devmode_message). You can see the get_devmode_option() function as an enhanced version of getOption() .

Looking for get_devmode_option() in the rest of Shiny’s source code, we see that it is used to retrieve a specific devmode option (e.g. "shiny.minified"), and act accordingly. For example in shinyui.R, this specific option is used to attach a non-minified js file when "shiny.minified" is FALSE:

				
					shinyDependencies <- function() {
  list(
    bslib::bs_dependency_defer(shinyDependencyCSS),
    busyIndicatorDependency(),
    htmlDependency(
      name = "shiny-javascript",
      version = get_package_version("shiny"),
      src = "www/shared",
      package = "shiny",
      script =
        if (isTRUE(
          get_devmode_option(
            "shiny.minified",
            TRUE
          )
        ))
          "shiny.min.js"
        else
          "shiny.js",
      all_files = FALSE
    )
  )
}
				
			

It works similar for "shiny.autoreload" and "shiny.fullstacktrace".

Mystery solved? Not quite… Developer mode should automatically turn on "shiny.minified", "shiny.autoreload" and "shiny.fullstacktrace", but we don’t see that in above code snippets. We do see itĀ here, in rlang’s on_load() function:

				
					on_load({
  register_devmode_option(
    "shiny.autoreload",
    "Turning on shiny autoreload. To disable, call `options(shiny.autoreload = FALSE)`",
    TRUE
  )

  register_devmode_option(
    "shiny.minified",
    "Using full shiny javascript file. To use the minified version, call `options(shiny.minified = TRUE)`",
    FALSE
  )

  register_devmode_option(
    "shiny.fullstacktrace",
    "Turning on full stack trace. To disable, call `options(shiny.fullstacktrace = FALSE)`",
    TRUE
  )
})
				
			

on_load() is part of rlang’s package lifecycle tools and is used to execute code when a package is loaded into memory. register_devmode_option() puts these options with their default values in a “store”, which the get_devmode_option() function retrieves with registered_devmode_options$get(). All of this only happens when in_devmode() is TRUE.

Right, can you still follow it? Let’s simplify it a little bit:

  1. Whenever the Shiny code needs values for the autoreload, minified or fullstacktrace options to determine how to behave, it calls get_devmode_option().
  2. Through this function, Shiny checks whether or not you’ve enabled devmode. If you didn’t, default values are returned, like FALSE for autoreload. If you did enable devmode, Shiny looks up the registered devmode options in a special “store”. In this case the value of autoreload will be TRUE.
  3. Running devmode(TRUE) will tell Shiny to enable developer mode, so get_devmode_option() can pick up on it.

Of course, as a Shiny user, you only have to worry about the devmode() function. You can just assume it works and sets the appropriate options. But let’s be honest: it’s fun to know how it actually works, right?!

Shiny Source Code Explained: other editions

And with that, this edition of “Shiny Source Code Explained” comes to an end. Do you want to keep on exploring Shiny’s Source Code? HereĀ are the previous editions:

Wrap up

I really enjoy writing this series, and I don’t expect anything in return (besides some eyeballs on my content, I’m not just crafting this series for myself … šŸ‘€). However, if you want to support me, you can:

ā˜•ļø Ā Buy me a coffee!

šŸ“š Take a look at my online Shiny courses: ProductioniZing Shiny , CustomiZing WidgetZĀ and Mobile StructureZ.

šŸ¤“ Let me help you with your R and Shiny questions. Do you need help with your project? Let’s have a chat!

Geef een reactie

Je e-mailadres wordt niet gepubliceerd. Vereiste velden zijn gemarkeerd met *