diff --git a/DESCRIPTION b/DESCRIPTION index 86bfa8c..17fc68b 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -45,7 +45,8 @@ Suggests: jsonlite, purrr, knitr, - rmarkdown + rmarkdown, + sentryR RdMacros: mathjaxr Config/testthat/edition: 3 Encoding: UTF-8 diff --git a/R/run-api.R b/R/run-api.R index 9030be7..32bf0a8 100644 --- a/R/run-api.R +++ b/R/run-api.R @@ -12,6 +12,15 @@ #' #' @export run_unbiased <- function() { + tryCatch( + { + rlang::global_entrace() + }, + error = function(e) { + message("Error setting up global_entrace, it is expected in testing environment: ", e$message) + } + ) + setup_sentry() host <- Sys.getenv("UNBIASED_HOST", "0.0.0.0") port <- as.integer(Sys.getenv("UNBIASED_PORT", "3838")) assign("db_connection_pool", @@ -38,3 +47,62 @@ run_unbiased <- function() { plumber::pr_run(host = host, port = port) } } + +# hack to make sure we can mock the globalCallingHandlers +# this method needs to be present in the package environment for mocking to work +# linter disabled intentionally since this is internal method and cannot be renamed +globalCallingHandlers <- NULL # nolint + +#' setup_sentry function +#' +#' This function is used to configure Sentry, a service for real-time error tracking. +#' It uses the sentryR package to set up Sentry based on environment variables. +#' +#' @param None +#' +#' @return None. If the SENTRY_DSN environment variable is not set, the function will +#' return a message and stop execution. +#' +#' @examples +#' setup_sentry() +#' +#' @details +#' The function first checks if the SENTRY_DSN environment variable is set. If not, it +#' returns a message and stops execution. +#' If SENTRY_DSN is set, it uses the sentryR::configure_sentry function to set up Sentry with +#' the following parameters: +#' - dsn: The Data Source Name (DSN) is retrieved from the SENTRY_DSN environment variable. +#' - app_name: The application name is set to "unbiased". +#' - app_version: The application version is retrieved from the GITHUB_SHA environment variable. +#' If not set, it defaults to "unspecified". +#' - environment: The environment is retrieved from the SENTRY_ENVIRONMENT environment variable. +#' If not set, it defaults to "development". +#' - release: The release is retrieved from the SENTRY_RELEASE environment variable. +#' If not set, it defaults to "unspecified". +#' +#' @seealso \url{https://docs.sentry.io/} +setup_sentry <- function() { + sentry_dsn <- Sys.getenv("SENTRY_DSN") + if (sentry_dsn == "") { + message("SENTRY_DSN not set, skipping Sentry setup") + return() + } + + sentryR::configure_sentry( + dsn = sentry_dsn, + app_name = "unbiased", + app_version = Sys.getenv("GITHUB_SHA", "unspecified"), + environment = Sys.getenv("SENTRY_ENVIRONMENT", "development"), + release = Sys.getenv("SENTRY_RELEASE", "unspecified") + ) + + globalCallingHandlers( + error = global_calling_handler + ) +} + +global_calling_handler <- function(error) { + error$function_calls <- sys.calls() + sentryR::capture_exception(error) + signalCondition(error) +} diff --git a/README.md b/README.md index 5eadab3..9a3dc09 100644 --- a/README.md +++ b/README.md @@ -54,4 +54,11 @@ To calculate code coverage, you will need to install the `covr` package. Once in - `covr::report()`: This method runs all tests and generates a detailed coverage report in HTML format. - `covr::package_coverage()`: This method provides a simpler, text-based code coverage report. -Alternatively, you can use the provided `run_tests_with_coverage.sh` script to run Unbiased tests with code coverage. \ No newline at end of file +Alternatively, you can use the provided `run_tests_with_coverage.sh` script to run Unbiased tests with code coverage. + +# Configuring Sentry +The Unbiased server offers robust error reporting capabilities through the integration of the Sentry service. To activate Sentry, simply set the `SENTRY_DSN` environment variable. Additionally, you have the flexibility to customize the setup further by configuring the following environment variables: + +* `SENTRY_ENVIRONMENT` This is used to set the environment (e.g., "production", "staging", "development"). If not set, the environment defaults to "development". + +* `SENTRY_RELEASE` This is used to set the release in Sentry. If not set, the release defaults to "unspecified". \ No newline at end of file diff --git a/inst/plumber/unbiased_api/meta.R b/inst/plumber/unbiased_api/meta.R index 171e191..09622bf 100644 --- a/inst/plumber/unbiased_api/meta.R +++ b/inst/plumber/unbiased_api/meta.R @@ -6,7 +6,7 @@ #* @tag other #* @get /sha #* @serializer unboxedJSON -function(res) { +sentryR::with_captured_calls(function(req, res) { sha <- Sys.getenv("GITHUB_SHA", unset = "NULL") if (sha == "NULL") { res$status <- 404 @@ -14,4 +14,4 @@ function(res) { } else { return(sha) } -} +}) diff --git a/inst/plumber/unbiased_api/plumber.R b/inst/plumber/unbiased_api/plumber.R index 3f2b07d..06add32 100644 --- a/inst/plumber/unbiased_api/plumber.R +++ b/inst/plumber/unbiased_api/plumber.R @@ -19,8 +19,10 @@ #* #* @plumber function(api) { - meta <- plumber::pr("meta.R") - study <- plumber::pr("study.R") + meta <- plumber::pr("meta.R") |> + plumber::pr_set_error(sentryR::sentry_error_handler) + study <- plumber::pr("study.R") |> + plumber::pr_set_error(sentryR::sentry_error_handler) api |> plumber::pr_mount("/meta", meta) |> diff --git a/inst/plumber/unbiased_api/study.R b/inst/plumber/unbiased_api/study.R index f613b4e..07e7f95 100644 --- a/inst/plumber/unbiased_api/study.R +++ b/inst/plumber/unbiased_api/study.R @@ -18,13 +18,15 @@ #* @post /minimisation_pocock #* @serializer unboxedJSON #* -function(identifier, name, method, arms, covariates, p, req, res) { +sentryR::with_captured_calls(function( + identifier, name, method, arms, covariates, p, req, res +) { return( unbiased:::api__minimization_pocock( identifier, name, method, arms, covariates, p, req, res ) ) -} +}) #* Randomize one patient #* @@ -37,8 +39,8 @@ function(identifier, name, method, arms, covariates, p, req, res) { #* @serializer unboxedJSON #* -function(study_id, current_state, req, res) { +sentryR::with_captured_calls(function(study_id, current_state, req, res) { return( unbiased:::api__randomize_patient(study_id, current_state, req, res) ) -} +}) diff --git a/man/setup_sentry.Rd b/man/setup_sentry.Rd new file mode 100644 index 0000000..8de319a --- /dev/null +++ b/man/setup_sentry.Rd @@ -0,0 +1,42 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/run-api.R +\name{setup_sentry} +\alias{setup_sentry} +\title{setup_sentry function} +\usage{ +setup_sentry() +} +\arguments{ +\item{None}{} +} +\value{ +None. If the SENTRY_DSN environment variable is not set, the function will +return a message and stop execution. +} +\description{ +This function is used to configure Sentry, a service for real-time error tracking. +It uses the sentryR package to set up Sentry based on environment variables. +} +\details{ +The function first checks if the SENTRY_DSN environment variable is set. If not, it +returns a message and stops execution. +If SENTRY_DSN is set, it uses the sentryR::configure_sentry function to set up Sentry with +the following parameters: +\itemize{ +\item dsn: The Data Source Name (DSN) is retrieved from the SENTRY_DSN environment variable. +\item app_name: The application name is set to "unbiased". +\item app_version: The application version is retrieved from the GITHUB_SHA environment variable. +If not set, it defaults to "unspecified". +\item environment: The environment is retrieved from the SENTRY_ENVIRONMENT environment variable. +If not set, it defaults to "development". +\item release: The release is retrieved from the SENTRY_RELEASE environment variable. +If not set, it defaults to "unspecified". +} +} +\examples{ +setup_sentry() + +} +\seealso{ +\url{https://docs.sentry.io/} +} diff --git a/renv.lock b/renv.lock index e2e732f..c4ecca1 100644 --- a/renv.lock +++ b/renv.lock @@ -2156,6 +2156,21 @@ ], "Hash": "c19df082ba346b0ffa6f833e92de34d1" }, + "sentryR": { + "Package": "sentryR", + "Version": "1.1.2", + "Source": "Repository", + "Repository": "RSPM", + "Requirements": [ + "httr", + "jsonlite", + "stats", + "stringr", + "tibble", + "uuid" + ], + "Hash": "f37e91d605fbf665d7b5467ded4e539e" + }, "sessioninfo": { "Package": "sessioninfo", "Version": "1.2.2", diff --git a/start_unbiased_api.sh b/start_unbiased_api.sh old mode 100644 new mode 100755 diff --git a/tests/testthat/setup-testing-environment.R b/tests/testthat/setup-testing-environment.R index fefd381..bc06c31 100644 --- a/tests/testthat/setup-testing-environment.R +++ b/tests/testthat/setup-testing-environment.R @@ -131,6 +131,10 @@ setup_test_db_connection_pool <- function(envir = parent.frame()) { ) } +# Make sure to disable Sentry during testing +withr::local_envvar( + SENTRY_DSN = NULL +) # We will always run the API on the localhost # and on a random port diff --git a/tests/testthat/test-run-api.R b/tests/testthat/test-run-api.R new file mode 100644 index 0000000..d5fd1bf --- /dev/null +++ b/tests/testthat/test-run-api.R @@ -0,0 +1,91 @@ +testthat::test_that("uses correct environment variables when setting up sentry", { + withr::local_envvar( + c( + SENTRY_DSN = "https://sentry.io/123", + GITHUB_SHA = "abc", + SENTRY_ENVIRONMENT = "production", + SENTRY_RELEASE = "1.0.0" + ) + ) + + testthat::local_mocked_bindings( + configure_sentry = function(dsn, + app_name, + app_version, + environment, + release) { + testthat::expect_equal(dsn, "https://sentry.io/123") + testthat::expect_equal(app_name, "unbiased") + testthat::expect_equal(app_version, "abc") + testthat::expect_equal(environment, "production") + testthat::expect_equal(release, "1.0.0") + }, + .package = "sentryR", + ) + + global_calling_handlers_called <- FALSE + + # mock globalCallingHandlers + testthat::local_mocked_bindings( + globalCallingHandlers = function(error) { + global_calling_handlers_called <<- TRUE + testthat::expect_equal( + unbiased:::global_calling_handler, + error + ) + }, + ) + + unbiased:::setup_sentry() + + testthat::expect_true(global_calling_handlers_called) +}) + +testthat::test_that("skips sentry setup if SENTRY_DSN is not set", { + withr::local_envvar( + c( + SENTRY_DSN = "" + ) + ) + + testthat::local_mocked_bindings( + configure_sentry = function(dsn, + app_name, + app_version, + environment, + release) { + # should not be called, so we fail the test + testthat::expect_true(FALSE) + }, + .package = "sentryR", + ) + + was_called <- FALSE + + # mock globalCallingHandlers + testthat::local_mocked_bindings( + globalCallingHandlers = function(error) { + was_called <<- TRUE + }, + ) + + testthat::expect_message(unbiased:::setup_sentry(), "SENTRY_DSN not set, skipping Sentry setup") + testthat::expect_false(was_called) +}) + +testthat::test_that("global_calling_handler captures exception and signals condition", { + error <- simpleError("test error") + + capture_exception_called <- FALSE + + testthat::local_mocked_bindings( + capture_exception = function(error) { + capture_exception_called <<- TRUE + testthat::expect_equal(error, error) + }, + .package = "sentryR", + ) + + testthat::expect_error(unbiased:::global_calling_handler(error)) + testthat::expect_true(capture_exception_called) +}) diff --git a/vignettes/articles/minimization_randomization_comparison.Rmd b/vignettes/articles/minimization_randomization_comparison.Rmd index 70758e1..bdac6eb 100644 --- a/vignettes/articles/minimization_randomization_comparison.Rmd +++ b/vignettes/articles/minimization_randomization_comparison.Rmd @@ -156,7 +156,8 @@ def <- simstudy::defData(def, varname = "hba1c", formula = "0.888", dist = "bina def <- simstudy::defData(def, varname = "tpo2", formula = "0.354", dist = "binary") # correlation with diabetes type def <- simstudy::defData( - def, varname = "age", formula = "(diabetes_type == 0) * (-0.95)", link = "logit", dist = "binary" + def, + varname = "age", formula = "(diabetes_type == 0) * (-0.95)", link = "logit", dist = "binary" ) # <= 2 - 0.302 def <- simstudy::defData(def, varname = "wound_size", formula = "0.302", dist = "binary")