rshinysentry

How to recreate the full traceback of an error in shiny that is printed in the R console?


I want to capture the full traceback that contains information about error in shiny. This can be useful if shiny is connected with the sentryR R package for reporting the errors on sentry dashboard. For example, clicking the Trigger Error button produces an error in this app:

library(shiny)
library(bslib)

ui <- page_fluid(
  title = "Error Demonstration App",
  card(
    card_header("Error Demo"),
    card_body(
      p("Click the button below to intentionally trigger an error:"),
      actionButton("trigger_error", "Trigger Error"),
      verbatimTextOutput("error_output")
    )
  )
)

server <- function(input, output, session) {
  
  observeEvent(input$trigger_error, {
    output$error_output <- renderPrint({
      # This will deliberately cause an error
      stop("HELLO!")
    })
  })
  
}

shinyApp(ui, server)
# Warning: Error in renderPrint: HELLO!
#   103: stop
#   102: renderPrint [#6]
#   101: func
#   85: renderFunc
#   84: output$error_output
#   3: runApp
#   2: print.shiny.appobj
#   1: <Anonymous>

Using sentryR:

library(shiny)
library(bslib)

ui <- page_fluid(
  title = "Error Demonstration App",
  card(
    card_header("Error Demo"),
    card_body(
      p("Click the button below to intentionally trigger an error:"),
      actionButton("trigger_error", "Trigger Error"),
      verbatimTextOutput("error_output")
    )
  )
)

server <- function(input, output, session) {
  
  # Configure sentry ---------------------------
  sentryR::configure_sentry(
    dsn = Sys.getenv("SENTRY_KEY"),
    app_name = "APP",
    app_version = "1.0.0"
  )
  
  # Function to safely get browser info ------------------
  get_browser_info <- function() {
    tryCatch(
      {
        user_agent <- session$request$HTTP_USER_AGENT # browser
        remote_addr <- session$request$REMOTE_ADDR # ip
        list(
          user_agent = if (is.null(user_agent)) "Unknown" else user_agent,
          remote_addr = if (is.null(remote_addr)) "Unknown" else remote_addr
        )
      },
      error = function(e) {
        list(
          user_agent = paste("Error getting user agent:", e$message),
          remote_addr = paste("Error getting IP:", e$message)
        )
      }
    )
  }
  
  # Custom error handler -----------------------
  error_handler <- function() {
    tryCatch(
      {
        err <- geterrmessage()
        browser <- get_browser_info()
        # Send the original error object with additional context as extra
        sentryR::capture(
          message = geterrmessage(),
          extra = list(browser = browser$user_agent, ip = browser$remote_addr)
        )
      },
      error = function(e) {
        print(paste("Error in error handler:", e$message))
      }
    )
  }
  
  # Shiny options ------------------------------
  options(shiny.error = error_handler)
  
  
    
  # App functionality ---------------------------
  observeEvent(input$trigger_error, {
    output$error_output <- renderPrint({
      # This will deliberately cause an error
      stop("HELLO!")
    })
  })
  
}

shinyApp(ui, server)
# Your event was recorded in Sentry with ID <long ID>
# Warning: Error in renderPrint: HELLO!
#   103: stop
#   102: renderPrint [#57]
#   101: func
#   85: renderFunc
#   84: output$error_output
#   3: runApp
#   2: print.shiny.appobj
#   1: <Anonymous>

The result is shown in the screenshot from sentry dashboard. enter image description here

As you can see, this is very useful, but it does not have any information about the traceback as seen in the R console. The only info about the error is in Message. Is there a way to gather the full traceback information in R, then I can send it to sentry? I have tried sys.calls() but it is verbose and does not really contain the same information as what we see in the R console.


Solution

  • This was answered by Tan Ho on DSLC slack. The key is to use shiny::printStackTrace. Here is the working example:

    library(shiny)
    library(bslib)
    
    ui <- page_fluid(
      title = "Error Demonstration App",
      card(
        card_header("Error Demo"),
        card_body(
          p("Click the button below to intentionally trigger an error:"),
          actionButton("trigger_error", "Trigger Error"),
          verbatimTextOutput("error_output")
        )
      )
    )
    
    server <- function(input, output, session) {
    
      # Configure sentry ---------------------------
      sentryR::configure_sentry(
        dsn = Sys.getenv("SENTRY_KEY"),
        app_name = "APP",
        app_version = "1.0.0"
      )
    
      # Function to safely get browser info ------------------
      get_browser_info <- function() {
        tryCatch(
          {
            user_agent <- session$request$HTTP_USER_AGENT # browser
            remote_addr <- session$request$REMOTE_ADDR # ip
            list(
              user_agent = if (is.null(user_agent)) "Unknown" else user_agent,
              remote_addr = if (is.null(remote_addr)) "Unknown" else remote_addr
            )
          },
          error = function(e) {
            list(
              user_agent = paste("Error getting user agent:", e$message),
              remote_addr = paste("Error getting IP:", e$message)
            )
          }
        )
      }
    
    
    
      # Custom error handler -----------------------
      error_handler <- function() {
        tryCatch(
          {
            e <- get("e", envir = parent.frame())
    
            stack_trace <- shiny::printStackTrace(e) |>
              capture.output(type = "message") |>
              list()
            browser <- get_browser_info()
    
            sentryR::capture(
              message = geterrmessage(),
              extra = list(Browser = browser$user_agent,
                           IP = browser$remote_addr,
                           "Stack trace" =  stack_trace)
            )
          },
          error = function(e) {
            print(paste("Error in error handler:", e$message))
          }
        )
      }
    
    
      # Shiny options ------------------------------
      options(shiny.error = error_handler)
    
    
    
      # App functionality ---------------------------
      observeEvent(input$trigger_error, {
        output$error_output <- renderPrint({
          # This will deliberately cause an error
          stop("HELLO!")
        })
      })
    
    }
    
    shinyApp(ui, server)
    

    This produces the exact same traceback on sentry dashboard that is seen in the R console.