Merge branch 'main' into rc-v1.12.0

This commit is contained in:
Barret Schloerke
2025-11-06 11:20:56 -05:00
committed by GitHub
39 changed files with 523 additions and 702 deletions

View File

@@ -91,7 +91,7 @@ Imports:
lifecycle (>= 0.2.0),
mime (>= 0.3),
otel,
promises (>= 1.4.0),
promises (>= 1.5.0),
R6 (>= 2.0),
rlang (>= 0.4.10),
sourcetools,

View File

@@ -387,14 +387,13 @@ importFrom(lifecycle,is_present)
importFrom(promises,"%...!%")
importFrom(promises,"%...>%")
importFrom(promises,as.promise)
importFrom(promises,hybrid_then)
importFrom(promises,is.promise)
importFrom(promises,is.promising)
importFrom(promises,local_ospan_promise_domain)
importFrom(promises,promise)
importFrom(promises,new_promise_domain)
importFrom(promises,promise_reject)
importFrom(promises,promise_resolve)
importFrom(promises,then)
importFrom(promises,with_ospan_async)
importFrom(promises,with_ospan_promise_domain)
importFrom(promises,with_promise_domain)
importFrom(rlang,"%||%")
importFrom(rlang,"fn_body<-")
importFrom(rlang,"fn_fmls<-")

View File

@@ -520,8 +520,7 @@ bindCache.reactiveExpr <- function(x, ..., cache = "app") {
local({
impl <- attr(res, "observable", exact = TRUE)
impl$.otelAttrs <- x_otel_attrs
impl$.otelAttrs <- append_otel_srcref_attrs(impl$.otelAttrs, call_srcref)
impl$.otelAttrs <- append_otel_srcref_attrs(x_otel_attrs, call_srcref)
})
if (has_otel_bind("reactivity")) {

View File

@@ -240,8 +240,7 @@ bindEvent.reactiveExpr <- function(x, ..., ignoreNULL = TRUE, ignoreInit = FALSE
local({
impl <- attr(res, "observable", exact = TRUE)
impl$.otelAttrs <- x_otel_attrs
impl$.otelAttrs <- append_otel_srcref_attrs(impl$.otelAttrs, call_srcref)
impl$.otelAttrs <- append_otel_srcref_attrs(x_otel_attrs, call_srcref)
})

View File

@@ -134,7 +134,9 @@ getCallCategories <- function(calls) {
#' @rdname stacktrace
#' @export
captureStackTraces <- function(expr) {
promises::with_promise_domain(createStackTracePromiseDomain(),
# Use `promises::` as it shows up in the stack trace
promises::with_promise_domain(
createStackTracePromiseDomain(),
expr
)
}
@@ -184,7 +186,7 @@ createStackTracePromiseDomain <- function() {
# These are actually stateless, we wouldn't have to create a new one each time
# if we didn't want to. They're pretty cheap though.
d <- promises::new_promise_domain(
d <- new_promise_domain(
wrapOnFulfilled = function(onFulfilled) {
force(onFulfilled)
# Subscription time
@@ -278,7 +280,7 @@ withLogErrors <- function(expr,
result <- captureStackTraces(expr)
# Handle expr being an async operation
if (promises::is.promise(result)) {
if (is.promise(result)) {
result <- promises::catch(result, function(cond) {
# Don't print shiny.silent.error (i.e. validation errors)
if (cnd_inherits(cond, "shiny.silent.error")) {

View File

@@ -136,25 +136,13 @@ ExtendedTask <- R6Class("ExtendedTask", portable = TRUE, cloneable = FALSE,
call_srcref,
defaultLabel = "<anonymous>"
)
private$otel_label <- otel_label_extended_task(label, domain = domain)
private$otel_label_add_to_queue <- otel_label_extended_task_add_to_queue(label, domain = domain)
private$otel_span_label <- otel_span_label_extended_task(label, domain = domain)
private$otel_log_label_add_to_queue <- otel_log_label_extended_task_add_to_queue(label, domain = domain)
private$otel_attrs <- c(
otel_srcref_attributes(call_srcref),
otel_session_id_attrs(domain)
) %||% list()
set_rv_label <- function(rv, suffix) {
impl <- attr(rv, ".impl", exact = TRUE)
impl$.otelLabel <- otel_label_extended_task_set_reactive_val(
label,
suffix,
domain = domain
)
}
set_rv_label(private$rv_status, "status")
set_rv_label(private$rv_value, "value")
set_rv_label(private$rv_error, "error")
},
#' @description
#' Starts executing the long-running operation. If this `ExtendedTask` is
@@ -175,7 +163,7 @@ ExtendedTask <- R6Class("ExtendedTask", portable = TRUE, cloneable = FALSE,
private$invocation_queue$size() > 0
) {
otel_log(
private$otel_label_add_to_queue,
private$otel_log_add_to_queue_label,
severity = "debug",
attributes = c(
private$otel_attrs,
@@ -188,11 +176,11 @@ ExtendedTask <- R6Class("ExtendedTask", portable = TRUE, cloneable = FALSE,
} else {
if (has_otel_bind("reactivity")) {
private$ospan <- create_shiny_ospan(
private$otel_label,
private$otel_span <- start_otel_span(
private$otel_span_label,
attributes = private$otel_attrs
)
otel::local_active_span(private$ospan)
otel::local_active_span(private$otel_span)
}
private$do_invoke(args, call = call)
@@ -262,33 +250,34 @@ ExtendedTask <- R6Class("ExtendedTask", portable = TRUE, cloneable = FALSE,
rv_error = NULL,
invocation_queue = NULL,
otel_label = NULL,
otel_span_label = NULL,
otel_log_label_add_to_queue = NULL,
otel_attrs = list(),
otel_label_add_to_queue = NULL,
ospan = NULL,
otel_span = NULL,
do_invoke = function(args, call = NULL) {
private$rv_status("running")
private$rv_value(NULL)
private$rv_error(NULL)
p <- promises::promise_resolve(
p <- promise_resolve(
maskReactiveContext(do.call(private$func, args))
)
p <- promises::then(
p,
onFulfilled = function(value, .visible) {
if (is_ospan(private$ospan)) {
private$ospan$end(status_code = "ok")
private$ospan <- NULL
if (is_otel_span(private$otel_span)) {
private$otel_span$end(status_code = "ok")
private$otel_span <- NULL
}
private$on_success(list(value = value, visible = .visible))
},
onRejected = function(error) {
if (is_ospan(private$ospan)) {
private$ospan$end(status_code = "error")
private$ospan <- NULL
if (is_otel_span(private$otel_span)) {
private$otel_span$end(status_code = "error")
private$otel_span <- NULL
}
private$on_error(error, call = call)
}

View File

@@ -100,7 +100,7 @@ plotPNG <- function(func, filename=tempfile(fileext='.png'),
createGraphicsDevicePromiseDomain <- function(which = dev.cur()) {
force(which)
promises::new_promise_domain(
new_promise_domain(
wrapOnFulfilled = function(onFulfilled) {
force(onFulfilled)
function(...) {

View File

@@ -442,15 +442,15 @@ MockShinySession <- R6Class(
prom <- NULL
tryCatch({
v <- private$withCurrentOutput(name, func(self, name))
if (!promises::is.promise(v)){
if (!is.promise(v)){
# Make our sync value into a promise
prom <- promises::promise(function(resolve, reject){ resolve(v) })
prom <- promise_resolve(v)
} else {
prom <- v
}
}, error=function(e){
# Error running value()
prom <<- promises::promise(function(resolve, reject){ reject(e) })
prom <<- promise_reject(e)
})
private$outs[[name]]$promise <- hybrid_chain(
@@ -718,7 +718,7 @@ MockShinySession <- R6Class(
stop("Nested calls to withCurrentOutput() are not allowed.")
}
promises::with_promise_domain(
with_promise_domain(
createVarPromiseDomain(private, "currentOutputName", name),
expr
)

View File

@@ -41,23 +41,17 @@ get_call_srcref <- function(which_offset = 0) {
}
append_otel_attrs <- function(attrs, new_attrs) {
if (is.null(new_attrs)) {
return(attrs)
}
attrs[names(new_attrs)] <- new_attrs
attrs
}
append_otel_srcref_attrs <- function(attrs, call_srcref) {
if (is.null(call_srcref)) {
return(attrs)
}
srcref_attrs <- otel_srcref_attributes(call_srcref)
attrs <- append_otel_attrs(attrs, srcref_attrs)
if (is.null(srcref_attrs)) {
return(attrs)
}
attrs[names(srcref_attrs)] <- srcref_attrs
attrs
}

View File

@@ -1,41 +1,3 @@
# - OpenTelemetry -----------------------------------
# * Integration locations:
# * √ Server:
# * Start reactive_update when reactive busy count > 0
# * End reactive_update when reactive busy count == 0
# * √ Reactives: val, values, expr, render fn, observe
# * Combinations:
# * √ debounce() / throttle()
# * bindCache()
# * √ bindEvent()
# * X - bindProgress()
# * Special functions:
# * ExtendedTask()
# * Extended task links to submission reactive
# * Reactive update that gets result links to the extended task
# * √ observeEvent()
# * √ eventReactive()
# * TODO: Not recording updates within the span!!
# * Maybe enhance all `withReactiveDomain()` calls?
# * Global options:
# * √ shiny.otel.bind:
# * "all", "none" - all or nothing
# * "session" - Adds session start/end events
# * "reactive_update" - Spans for any reactive update. (Includes `"session"` features).
# * "reactivity" - Spans for all reactive things. (Includes `"reactive_update"` features).
# * Private methods:
# * bind_otel_*() - Methods that binds the reactive object to OpenTelemetry spans
# * Note: When adding otel to an object, prepend a class of `FOO.otel`. Then add a dispatch method for `bindOtel.FOO.otel()` that declares the object already has been bound.
# * with_no_otel_bind(expr) - Will not bind any reactives created within `expr` to OpenTelemetry spans.
# - TODO: -----------------------------------
# * Span status for success/failure (render function and regular reactive exprs?)
# * Error handling is not an "exception" for fatal logs
# * Connect `user.id` to be their user name: https://opentelemetry.io/docs/specs/semconv/registry/attributes/user/
# * Tests with otel recording
# ------------------------------------------
otel_bind_choices <- c(
"none",
"session",
@@ -151,8 +113,8 @@ as_otel_bind <- function(bind = "all") {
#' @section Span management and performance:
#'
#' Dev note - Barret 2025-10:
#' Typically, an OpenTelemetry span (ospan) will inherit from the parent span.
#' This works well and we can think of the hierarchy as a tree. With
#' Typically, an OpenTelemetry span (`otel_span`) will inherit from the parent
#' span. This works well and we can think of the hierarchy as a tree. With
#' `options("shiny.otel.bind" = <value>)`, we are able to control with a sliding
#' dial how much of the tree we are interested in: "none", "session",
#' "reactive_update", "reactivity", and finally "all".
@@ -230,7 +192,7 @@ bind_otel_reactive_expr <- function(x) {
impl <- attr(x, "observable", exact = TRUE)
impl$.isRecordingOtel <- TRUE
# Covers both reactive and reactive.event
impl$.otelLabel <- ospan_label_reactive(x, domain = impl$.domain)
impl$.otelLabel <- otel_span_label_reactive(x, domain = impl$.domain)
class(x) <- c("reactiveExpr.otel", class(x))
@@ -239,7 +201,7 @@ bind_otel_reactive_expr <- function(x) {
bind_otel_observe <- function(x) {
x$.isRecordingOtel <- TRUE
x$.otelLabel <- ospan_label_observer(x, domain = x$.domain)
x$.otelLabel <- otel_span_label_observer(x, domain = x$.domain)
class(x) <- c("Observer.otel", class(x))
invisible(x)
@@ -250,32 +212,32 @@ bind_otel_observe <- function(x) {
bind_otel_shiny_render_function <- function(x) {
valueFunc <- force(x)
span_label <- NULL
ospan_attrs <- NULL
otel_span_label <- NULL
otel_span_attrs <- NULL
renderFunc <- function(...) {
# Dynamically determine the span label given the current reactive domain
if (is.null(span_label)) {
if (is.null(otel_span_label)) {
domain <- getDefaultReactiveDomain()
span_label <<-
ospan_label_render_function(x, domain = domain)
ospan_attrs <<- c(
otel_span_label <<-
otel_span_label_render_function(x, domain = domain)
otel_span_attrs <<- c(
attr(x, "otelAttrs"),
otel_session_id_attrs(domain)
)
}
with_shiny_ospan_async(
span_label,
with_otel_span(
otel_span_label,
{
promises::hybrid_then(
hybrid_then(
valueFunc(...),
on_failure = set_ospan_error_status_and_throw,
on_failure = set_otel_exception_status_and_throw,
# Must save the error object
tee = FALSE
)
},
attributes = ospan_attrs
attributes = otel_span_attrs
)
}

View File

@@ -1,21 +1,21 @@
has_seen_ospan_error <- function(cnd) {
isTRUE(cnd$.shiny_error_seen)
has_seen_otel_exception <- function(cnd) {
!is.null(cnd$.shiny_otel_exception)
}
set_ospan_error_as_seen <- function(cnd) {
cnd$.shiny_error_seen <- TRUE
mark_otel_exception_as_seen <- function(cnd) {
cnd$.shiny_otel_exception <- TRUE
cnd
}
set_ospan_error_status_and_throw <- function(cnd) {
cnd <- set_ospan_error_status(cnd)
set_otel_exception_status_and_throw <- function(cnd) {
cnd <- set_otel_exception_status(cnd)
# Rethrow the (possibly updated) error
signalCondition(cnd)
}
set_ospan_error_status <- function(cnd) {
set_otel_exception_status <- function(cnd) {
if (inherits(cnd, "shiny.custom.error")) {
# No-op
} else if (inherits(cnd, "shiny.output.cancel")) {
@@ -30,12 +30,12 @@ set_ospan_error_status <- function(cnd) {
# Only record the exception once at the original point of failure,
# not every reactive expression that it passes through
if (!has_seen_ospan_error(cnd)) {
if (!has_seen_otel_exception(cnd)) {
span$record_exception(
# Record a sanitized error if sanitization is enabled
get_otel_error_obj(cnd)
)
cnd <- set_ospan_error_as_seen(cnd)
cnd <- mark_otel_exception_as_seen(cnd)
}
# Record the error status on the span for any context touching this error

View File

@@ -1,6 +1,6 @@
# observe mymod:<anonymous>
# observe <anonymous>
# observe mylabel (edited)
# observe mylabel
# -- Reactives --------------------------------------------------------------
@@ -21,7 +21,7 @@
#' @noRd
NULL
ospan_label_reactive <- function(x, ..., domain) {
otel_span_label_reactive <- function(x, ..., domain) {
fn_name <- otel_label_with_modifiers(
x,
"reactive",
@@ -30,12 +30,12 @@ ospan_label_reactive <- function(x, ..., domain) {
)
label <- attr(x, "observable", exact = TRUE)[[".label"]]
ospan_label <- otel_label_upgrade(label, domain = domain)
otel_span_label <- otel_label_upgrade(label, domain = domain)
sprintf("%s %s", fn_name, ospan_label)
sprintf("%s %s", fn_name, otel_span_label)
}
ospan_label_render_function <- function(x, ..., domain) {
otel_span_label_render_function <- function(x, ..., domain) {
fn_name <- otel_label_with_modifiers(
x,
"output",
@@ -44,12 +44,12 @@ ospan_label_render_function <- function(x, ..., domain) {
)
label <- getCurrentOutputInfo(session = domain)$name %||% "<unknown>"
ospan_label <- otel_label_upgrade(label, domain = domain)
otel_span_label <- otel_label_upgrade(label, domain = domain)
sprintf("%s %s", fn_name, ospan_label)
sprintf("%s %s", fn_name, otel_span_label)
}
ospan_label_observer <- function(x, ..., domain) {
otel_span_label_observer <- function(x, ..., domain) {
fn_name <- otel_label_with_modifiers(
x,
"observe",
@@ -57,21 +57,21 @@ ospan_label_observer <- function(x, ..., domain) {
event_class = "Observer.event"
)
ospan_label <- otel_label_upgrade(x$.label, domain = domain)
otel_span_label <- otel_label_upgrade(x$.label, domain = domain)
sprintf("%s %s", fn_name, ospan_label)
sprintf("%s %s", fn_name, otel_span_label)
}
# -- Set reactive value(s) ----------------------------------------------------
otel_label_set_reactive_val <- function(label, ..., domain) {
otel_log_label_set_reactive_val <- function(label, ..., domain) {
sprintf(
"Set reactiveVal %s",
otel_label_upgrade(label, domain = domain)
)
}
otel_label_set_reactive_values <- function(label, key, ..., domain) {
otel_log_label_set_reactive_values <- function(label, key, ..., domain) {
sprintf(
"Set reactiveValues %s$%s",
otel_label_upgrade(label, domain = domain),
@@ -81,23 +81,16 @@ otel_label_set_reactive_values <- function(label, key, ..., domain) {
# -- ExtendedTask -------------------------------------------------------------
otel_label_extended_task <- function(label, suffix = NULL, ..., domain) {
otel_span_label_extended_task <- function(label, suffix = NULL, ..., domain) {
sprintf(
"ExtendedTask %s",
otel_label_upgrade(label, domain = domain)
)
}
otel_label_extended_task_add_to_queue <- function(label, ..., domain) {
otel_log_label_extended_task_add_to_queue <- function(label, ..., domain) {
sprintf(
"%s add to queue",
otel_label_extended_task(label, domain = domain)
)
}
otel_label_extended_task_set_reactive_val <- function(label, name, ..., domain) {
sprintf(
"Set %s %s",
otel_label_extended_task(label, domain = domain),
name
"ExtendedTask %s add to queue",
otel_label_upgrade(label, domain = domain)
)
}

View File

@@ -1,79 +1,59 @@
OSPAN_REACTIVE_UPDATE_NAME <- "reactive_update"
# * `session$userData[["_otel_reactive_update_ospan"]]` - The active reactive update span (or `NULL`)
# * `session$userData[["_otel_has_reactive_cleanup"]]` - Whether the reactive span cleanup has been set
has_reactive_ospan_cleanup <- function(domain) {
isTRUE(domain$userData[["_otel_has_reactive_cleanup"]])
}
set_reactive_ospan_cleanup <- function(domain) {
domain$userData[["_otel_has_reactive_cleanup"]] <- TRUE
}
# * `session$userData[["_otel_span_reactive_update"]]` - The active reactive update span (or `NULL`)
reactive_update_ospan_is_active <- function(domain) {
isTRUE(domain$userData[["_otel_reactive_update_is_active"]])
}
set_reactive_ospan_is_active <- function(domain) {
domain$userData[["_otel_reactive_update_is_active"]] <- TRUE
}
clear_reactive_ospan_is_active <- function(domain) {
domain$userData[["_otel_reactive_update_is_active"]] <- NULL
}
#' Create a `reactive_update` OpenTelemetry span
#' Start a `reactive_update` OpenTelemetry span and store it
#'
#' Used when a reactive expression is updated
#' Will only start the span iff the otel tracing is enabled
#' @param ... Ignored
#' @param domain The reactive domain to associate with the span
#' @return Invisibly returns.
#' @seealso `end_reactive_update_ospan()`
#' @seealso `otel_span_reactive_update_teardown()`
#' @noRd
create_reactive_update_ospan <- function(..., domain) {
otel_span_reactive_update_init <- function(..., domain) {
if (!has_otel_bind("reactive_update")) return()
if (!has_reactive_ospan_cleanup(domain)) {
# Clean up any dangling reactive span
# Ensure cleanup is registered only once per session
if (is.null(domain$userData[["_otel_has_reactive_cleanup"]])) {
domain$userData[["_otel_has_reactive_cleanup"]] <- TRUE
# Clean up any dangling reactive spans on an unplanned exit
domain$onSessionEnded(function() {
if (has_reactive_ospan_cleanup(domain)) {
end_reactive_update_ospan(domain = domain)
}
otel_span_reactive_update_teardown(domain = domain)
})
set_reactive_ospan_cleanup(domain)
}
prev_ospan <- domain$userData[["_otel_reactive_update_ospan"]]
if (is_ospan(prev_ospan)) {
# Safety check
if (is_otel_span(domain$userData[["_otel_span_reactive_update"]])) {
stop("Reactive update span already exists")
}
reactive_update_ospan <- create_shiny_ospan(
OSPAN_REACTIVE_UPDATE_NAME,
...,
# options = list(
# parent = NA # Always start a new root span
# ),
attributes = otel_session_id_attrs(domain)
)
domain$userData[["_otel_span_reactive_update"]] <-
start_otel_span(
"reactive_update",
...,
attributes = otel_session_id_attrs(domain)
)
domain$userData[["_otel_reactive_update_ospan"]] <- reactive_update_ospan
return(invisible())
invisible()
}
#' End a `reactive_update` OpenTelemetry span
#' End a `reactive_update` OpenTelemetry span and remove it from the session
#' @param ... Ignored
#' @param domain The reactive domain to associate with the span
#' @return Invisibly returns.
#' @seealso `create_reactive_update_ospan()`
#' @seealso `otel_span_reactive_update_init()`
#' @noRd
end_reactive_update_ospan <- function(..., domain) {
otel_span_reactive_update_teardown <- function(..., domain) {
ospan <- domain$userData[["_otel_span_reactive_update"]]
reactive_update_ospan <- domain$userData[["_otel_reactive_update_ospan"]]
if (is_ospan(reactive_update_ospan)) {
otel::end_span(reactive_update_ospan)
domain$userData[["_otel_reactive_update_ospan"]] <- NULL
if (is_otel_span(ospan)) {
otel::end_span(ospan)
domain$userData[["_otel_span_reactive_update"]] <- NULL
}
invisible()
}
@@ -85,42 +65,47 @@ end_reactive_update_ospan <- function(..., domain) {
#' @param ... Ignored
#' @param domain The reactive domain to associate with the span
#' @noRd
with_reactive_update_active_ospan <- function(expr, ..., domain) {
reactive_update_ospan <- domain$userData[["_otel_reactive_update_ospan"]]
with_otel_span_reactive_update <- function(expr, ..., domain) {
ospan <- domain$userData[["_otel_span_reactive_update"]]
if (!is_ospan(reactive_update_ospan)) {
if (!is_otel_span(ospan)) {
return(force(expr))
}
# Given the reactive span is started before and ended when exec count is 0,
# we only need to wrap the expr in the span context
otel::with_active_span(reactive_update_ospan, {force(expr)})
# Given the reactive update span is started before and ended when exec count
# is 0, we only need to wrap the expr in the span context
otel::with_active_span(ospan, {force(expr)})
}
#' Run expr within `reactive_update` ospan if not already active
#' Run expr within `reactive_update` otel span if not already active
#'
#' If the reactive update ospan is not already active, run the expression
#' within the reactive update ospan context. This ensures that nested calls
#' If the reactive update otel span is not already active, run the expression
#' within the reactive update otel span context. This ensures that nested calls
#' to reactive expressions do not attempt to re-enter the same span.
#'
#' This method is used within Context `run()` and running an Output's observer
#' implementation
#' @param expr The expression to executed within the span
#' @param ... Ignored
#' @param domain The reactive domain to associate with the span
#' @noRd
maybe_with_reactive_update_active_ospan <- function(expr, ..., domain) {
if (!reactive_update_ospan_is_active(domain)) {
set_reactive_ospan_is_active(domain)
maybe_with_otel_span_reactive_update <- function(expr, ..., domain) {
if (is.null(domain$userData[["_otel_reactive_update_is_active"]])) {
domain$userData[["_otel_reactive_update_is_active"]] <- TRUE
promises::hybrid_then(
# When the expression is done promising, clear the active flag
hybrid_then(
{
with_reactive_update_active_ospan(domain = domain, expr)
with_otel_span_reactive_update(domain = domain, expr)
},
on_success = function(value) {
clear_reactive_ospan_is_active(domain)
domain$userData[["_otel_reactive_update_is_active"]] <- NULL
},
on_failure = function(e) {
clear_reactive_ospan_is_active(domain)
domain$userData[["_otel_reactive_update_is_active"]] <- NULL
},
# Return the value before the callbacks
tee = TRUE
)
} else {

View File

@@ -10,33 +10,31 @@
#' @param ... Ignored
#' @param domain The reactive domain
#' @noRd
use_session_start_ospan_async <- function(expr, ..., domain) {
otel_span_session_start <- function(expr, ..., domain) {
if (!has_otel_bind("session")) {
return(force(expr))
}
id_attrs <- otel_session_id_attrs(domain)
# Wrap the server initialization
with_shiny_ospan_async(
with_otel_span(
"session_start",
expr,
attributes = otel::as_attributes(c(
id_attrs,
otel_session_id_attrs(domain),
otel_session_attrs(domain)
))
)
}
with_session_end_ospan_async <- function(expr, ..., domain) {
otel_span_session_end <- function(expr, ..., domain) {
if (!has_otel_bind("session")) {
return(force(expr))
}
id_attrs <- otel_session_id_attrs(domain)
with_shiny_ospan_async(
with_otel_span(
"session_end",
expr,
attributes = id_attrs
@@ -48,25 +46,33 @@ with_session_end_ospan_async <- function(expr, ..., domain) {
# Occurs when the websocket connection is established
otel_session_attrs <- function(domain) {
# TODO: Future: Posit Connect integration
# > we are still trying to identify all of the information we want to track/expose
#
# * `POSIT_PRODUCT` (Fallback to RSTUDIO_PRODUCT) for host environment
# * `CONNECT_SERVER` envvar to get the `session.address`.
# * `CONNECT_CONTENT_GUID` for the consistent app distinguisher
# * Maybe `CONNECT_CONTENT_JOB_KEY`?
# * Maybe `user.id` to be their user name: https://opentelemetry.io/docs/specs/semconv/registry/attributes/user/
attrs <- list(
PATH_INFO =
server.path =
sub(
"/websocket/$", "/",
domain[["request"]][["PATH_INFO"]] %||% ""
),
HTTP_HOST = domain[["request"]][["HTTP_HOST"]] %||% "",
HTTP_ORIGIN = domain[["request"]][["HTTP_ORIGIN"]] %||% "",
server.address = domain[["request"]][["HTTP_HOST"]] %||% "",
server.origin = domain[["request"]][["HTTP_ORIGIN"]] %||% "",
## Currently, Shiny does not expose QUERY_STRING when connecting the websocket
# so we do not provide it here.
# QUERY_STRING = domain[["request"]][["QUERY_STRING"]] %||% "",
SERVER_PORT = domain[["request"]][["SERVER_PORT"]] %||% NA_integer_
server.port = domain[["request"]][["SERVER_PORT"]] %||% NA_integer_
)
# Safely convert SERVER_PORT to integer
# If conversion fails, leave as-is (string or empty)
# This avoids warnings/errors if SERVER_PORT is not a valid integer
server_port <- suppressWarnings(as.integer(attrs$SERVER_PORT))
server_port <- suppressWarnings(as.integer(attrs$server.port))
if (!is.na(server_port)) {
attrs$SERVER_PORT <- server_port
attrs$server.port <- server_port
}
attrs

View File

@@ -1,29 +1,44 @@
#' @importFrom promises
#' with_ospan_async
#' with_ospan_promise_domain
#' local_ospan_promise_domain
NULL
# Used by otel to identify the tracer and logger for this package
# https://github.com/r-lib/otel/blob/afc31bc1f4bd177870d44b051ada1d9e4e685346/R/tracer-name.R#L33-L49
# DO NOT CHANGE THIS VALUE without understanding the implications for existing telemetry data!
otel_tracer_name <- "co.posit.r-package.shiny"
with_shiny_ospan_async <- function(name, expr, ..., attributes = NULL) {
with_ospan_async(name, expr, ..., attributes = attributes, tracer = get_tracer())
#' Create and use a Shiny OpenTelemetry span
#'
#' If otel is disabled, the span will not be created,
#' however the expression will still be evaluated.
#' @param name Span name
#' @param expr Expression to evaluate within the span
#' @param ... Ignored
#' @param attributes Optional span attributes
#' @return The result of evaluating `expr`
#' @noRd
with_otel_span <- function(name, expr, ..., attributes = NULL) {
promises::with_otel_span(name, expr, ..., attributes = attributes, tracer = shiny_otel_tracer())
}
create_shiny_ospan <- function(name, ...) {
otel::start_span(name, ..., tracer = get_tracer())
#' Start a Shiny OpenTelemetry span
#'
#' @param name Span name
#' @param ... Additional arguments passed to `otel::start_span()`
#' @return An OpenTelemetry span
#' @noRd
start_otel_span <- function(name, ...) {
otel::start_span(name, ..., tracer = shiny_otel_tracer())
}
# # TODO: Set attributes on the current active span
# # 5. Set attributes on the current active span
# set_ospan_attrs(status = 200L)
# set_otel_span_attrs(status = 200L)
# -- Helpers --------------------------------------------------------------
is_ospan <- function(x) {
is_otel_span <- function(x) {
inherits(x, "otel_span")
}
@@ -32,26 +47,38 @@ testthat__is_testing <- function() {
identical(Sys.getenv("TESTTHAT"), "true")
}
#' Log a message using the Shiny OpenTelemetry logger
#'
#' @param msg The log message
#' @param ... Additional attributes to add to the log record
#' @param severity The log severity level (default: "info")
#' @param logger The OpenTelemetry logger to use (default: Shiny otel logger)
#' @return Invisibly returns.
#' @noRd
otel_log <- function(
msg,
...,
severity = "info",
logger = get_ospan_logger()
logger = shiny_otel_logger()
) {
otel::log(msg, ..., severity = severity, logger = logger)
}
otel_is_tracing_enabled <- function(tracer = get_tracer()) {
#' Check if OpenTelemetry tracing is enabled
#'
#' @param tracer The OpenTelemetry tracer to check (default: Shiny otel tracer)
#' @return `TRUE` if tracing is enabled, `FALSE` otherwise
#' @noRd
otel_is_tracing_enabled <- function(tracer = shiny_otel_tracer()) {
otel::is_tracing_enabled(tracer)
}
otel_get_logger <- function() {
otel::get_logger()
}
otel_get_tracer <- function() {
otel::get_tracer()
}
get_ospan_logger <- local({
#' Shiny OpenTelemetry logger
#'
#' Used for logging OpenTelemetry events via `otel_log()`
#' @return An OpenTelemetry logger
#' @noRd
shiny_otel_logger <- local({
logger <- NULL
# For internal testing purposes only
@@ -64,7 +91,7 @@ get_ospan_logger <- local({
return(logger)
}
this_logger <- otel_get_logger()
this_logger <- otel::get_logger()
if (testthat__is_testing()) {
# Don't cache the logger in unit tests. It interferes with logger provider
@@ -78,9 +105,17 @@ get_ospan_logger <- local({
# Inspired by httr2:::get_tracer().
# Using local scope avoids an environment object lookup on each call.
get_tracer <- local({
#' Shiny OpenTelemetry tracer
#'
#' Used for creating OpenTelemetry spans via `with_otel_span()` and
#' `start_otel_span()`
#'
#' Inspired by httr2:::get_tracer().
#' @return An OpenTelemetry tracer
#' @noRd
shiny_otel_tracer <- local({
# Using local scope avoids an environment object lookup on each call.
tracer <- NULL
# For internal testing purposes only
@@ -93,7 +128,7 @@ get_tracer <- local({
return(tracer)
}
this_tracer <- otel_get_tracer()
this_tracer <- otel::get_tracer()
if (testthat__is_testing()) {
# Don't cache the tracer in unit tests. It interferes with tracer provider

View File

@@ -31,7 +31,7 @@ ctx_otel_info_obj <- function(
)
}
with_context_ospan_async <- function(otel_info, expr, domain) {
with_otel_span_context <- function(otel_info, expr, domain) {
if (!otel_is_tracing_enabled()) {
return(force(expr))
}
@@ -43,16 +43,16 @@ with_context_ospan_async <- function(otel_info, expr, domain) {
# Always set the reactive update span as active
# This ensures that any spans created within the reactive context
# are at least children of the reactive update span
maybe_with_reactive_update_active_ospan(domain = domain, {
maybe_with_otel_span_reactive_update(domain = domain, {
if (isRecordingOtel) {
with_shiny_ospan_async(
with_otel_span(
otelLabel,
{
# Works with both sync and async expressions
# Needed for both observer and reactive contexts
promises::hybrid_then(
hybrid_then(
expr,
on_failure = set_ospan_error_status_and_throw,
on_failure = set_otel_exception_status_and_throw,
# Must upgrade the error object
tee = FALSE
)
@@ -114,9 +114,10 @@ Context <- R6Class(
run = function(func) {
"Run the provided function under this context."
# Use `promises::` as it shows up in the stack trace
promises::with_promise_domain(reactivePromiseDomain(), {
withReactiveDomain(.domain, {
with_context_ospan_async(.otel_info, domain = .domain, {
with_otel_span_context(.otel_info, domain = .domain, {
captureStackTraces({
env <- .getReactiveEnvironment()
rLog$enter(.reactId, id, .reactType, .domain)
@@ -296,7 +297,7 @@ wrapForContext <- function(func, ctx) {
}
reactivePromiseDomain <- function() {
promises::new_promise_domain(
new_promise_domain(
wrapOnFulfilled = function(onFulfilled) {
force(onFulfilled)

View File

@@ -97,9 +97,11 @@ getDefaultReactiveDomain <- function() {
#' @rdname domains
#' @export
withReactiveDomain <- function(domain, expr) {
# TODO: Integrate `promises:::with_otel_active_span_promise_domain(expr)`
promises::with_promise_domain(createVarPromiseDomain(.globals, "domain", domain), expr)
# Use `promises::` as it shows up in the stack trace
promises::with_promise_domain(
createVarPromiseDomain(.globals, "domain", domain),
expr
)
}
#

View File

@@ -92,7 +92,7 @@ ReactiveVal <- R6Class(
domain <- getDefaultReactiveDomain()
rLog$define(private$reactId, value, private$label, type = "reactiveVal", domain)
.otelLabel <<- otel_label_set_reactive_val(private$label, domain = domain)
.otelLabel <<- otel_log_label_set_reactive_val(private$label, domain = domain)
},
get = function() {
private$dependents$register()
@@ -446,10 +446,14 @@ ReactiveValues <- R6Class(
}
if ((!is.null(domain)) && .isRecordingOtel) {
# Do not include updates to input or clientData unless _some_ reactivity has occured
if (has_reactive_ospan_cleanup(domain) || !(.label == "input" || .label == "clientData")) {
if (
# Any reactiveValues (other than input or clientData) are fair game
!(.label == "input" || .label == "clientData") ||
# Do not include updates to input or clientData unless _some_ reactivity has occured
!is.null(domain$userData[["_otel_has_reactive_cleanup"]])
) {
otel_log(
otel_label_set_reactive_values(.label, key, domain = domain),
otel_log_label_set_reactive_values(.label, key, domain = domain),
severity = "info",
attributes = c(.otelAttrs, otel_session_id_attrs(domain))
)
@@ -1019,11 +1023,11 @@ Observable <- R6Class(
error = function(cond) {
if (.isRecordingOtel) {
# `cond` is too early in the stack to be updated by `ctx`'s
# `with_context_ospan_async()` where it calls
# `set_ospan_error_status_and_throw()` on eval error.
# `with_otel_span_context()` where it calls
# `set_otel_exception_status_and_throw()` on eval error.
# So we mark it as seen here.
# When the error is re-thrown later, it won't be a _new_ error
cond <- set_ospan_error_as_seen(cond)
cond <- mark_otel_exception_as_seen(cond)
}
# If an error occurs, we want to propagate the error, but we also
@@ -2209,7 +2213,7 @@ isolate <- function(expr) {
reactId <- rLog$noReactId
}
# Do not track ospans for `isolate()`
# Do not track otel spans for `isolate()`
ctx <- Context$new(getDefaultReactiveDomain(), '[isolate]', type='isolate', reactId = reactId)
on.exit(ctx$invalidate())
# Matching ..stacktraceon../..stacktraceoff.. pair

View File

@@ -253,7 +253,7 @@ drawPlot <- function(name, session, func, width, height, alt, pixelratio, res, .
hybrid_chain(
hybrid_chain(
promises::with_promise_domain(domain, {
with_promise_domain(domain, {
hybrid_chain(
func(),
function(value) {

View File

@@ -98,7 +98,7 @@ runApp <- function(
# * While this could be done at a lower level, it allows for _anything_ within
# shiny's control to allow for the opportunity to have otel active spans be
# reactivated upon promise domain restoration
local_ospan_promise_domain()
promises::local_otel_promise_domain()
on.exit({
handlerManager$clear()

View File

@@ -274,7 +274,7 @@ createAppHandlers <- function(httpHandlers, serverFuncSource) {
args <- argsForServerFunc(serverFunc, shinysession)
withReactiveDomain(shinysession, {
use_session_start_ospan_async(domain = shinysession, {
otel_span_session_start(domain = shinysession, {
do.call(
# No corresponding ..stacktraceoff; the server func is pure

View File

@@ -4,12 +4,12 @@
#' @importFrom lifecycle deprecated is_present
#' @importFrom grDevices dev.set dev.cur
#' @importFrom fastmap fastmap
#' @importFrom promises %...!%
#' @importFrom promises %...>%
#' @importFrom promises
#' promise promise_resolve promise_reject is.promising
#' then
#' as.promise
#' %...!% %...>%
#' as.promise is.promising is.promise
#' promise_resolve promise_reject
#' hybrid_then
#' with_promise_domain new_promise_domain
#' @importFrom rlang
#' quo enquo enquo0 as_function get_expr get_env new_function enquos
#' eval_tidy expr pairlist2 new_quosure enexpr as_quosure is_quosure inject

View File

@@ -428,7 +428,7 @@ ShinySession <- R6Class(
stop("Nested calls to withCurrentOutput() are not allowed.")
}
promises::with_promise_domain(
with_promise_domain(
createVarPromiseDomain(private, "currentOutputName", name),
expr
)
@@ -1058,7 +1058,7 @@ ShinySession <- R6Class(
# For fatal errors, always log.
# For non-fatal errors, only log if we haven't seen this error before.
if (close || !has_seen_ospan_error(e)) {
if (close || !has_seen_otel_exception(e)) {
otel_log(
if (close) "Fatal error" else "Unhandled error",
severity = if (close) "fatal" else "error",
@@ -1086,7 +1086,7 @@ ShinySession <- R6Class(
}
# ..stacktraceon matches with the top-level ..stacktraceoff..
withReactiveDomain(self, {
with_session_end_ospan_async(domain = self, {
otel_span_session_end(domain = self, {
private$closedCallbacks$invoke(onError = printError, ..stacktraceon = TRUE)
})
})
@@ -1167,11 +1167,10 @@ ShinySession <- R6Class(
# This shinyCallingHandlers should maybe be at a higher level,
# to include the $then/$catch calls below?
hybrid_chain(
# TODO: Move ospan wrapper here to capture return value
hybrid_chain(
{
private$withCurrentOutput(name, {
maybe_with_reactive_update_active_ospan({
maybe_with_otel_span_reactive_update({
shinyCallingHandlers(func())
}, domain = self)
})
@@ -2040,7 +2039,7 @@ ShinySession <- R6Class(
ext <- paste(".", ext, sep = "")
tmpdata <- tempfile(fileext = ext)
return(Context$new(getDefaultReactiveDomain(), '[download]')$run(function() {
promises::with_promise_domain(reactivePromiseDomain(), {
with_promise_domain(reactivePromiseDomain(), {
captureStackTraces({
self$incrementBusyCount()
hybrid_chain(
@@ -2213,7 +2212,7 @@ ShinySession <- R6Class(
rLog$asyncStart(domain = self)
private$sendMessage(busy = "busy")
create_reactive_update_ospan(domain = self)
otel_span_reactive_update_init(domain = self)
}
private$busyCount <- private$busyCount + 1L
},
@@ -2236,7 +2235,7 @@ ShinySession <- R6Class(
}
})
end_reactive_update_ospan(domain = self)
otel_span_reactive_update_teardown(domain = self)
}
}
)

View File

@@ -626,7 +626,7 @@ renderPrint <- function(expr, env = parent.frame(), quoted = FALSE,
domain <- createRenderPrintPromiseDomain(width)
hybrid_chain(
{
promises::with_promise_domain(domain, func())
with_promise_domain(domain, func())
},
function(value) {
res <- withVisible(value)
@@ -655,7 +655,7 @@ renderPrint <- function(expr, env = parent.frame(), quoted = FALSE,
createRenderPrintPromiseDomain <- function(width) {
f <- file()
promises::new_promise_domain(
new_promise_domain(
wrapOnFulfilled = function(onFulfilled) {
force(onFulfilled)
function(...) {

View File

@@ -1525,7 +1525,7 @@ promise_chain <- function(promise, ..., catch = NULL, finally = NULL,
}
if (!is.null(domain)) {
promises::with_promise_domain(domain, do(), replace = replace)
with_promise_domain(domain, do(), replace = replace)
} else {
do()
}
@@ -1542,7 +1542,7 @@ hybrid_chain <- function(expr, ..., catch = NULL, finally = NULL,
{
captureStackTraces({
result <- withVisible(force(expr))
if (promises::is.promising(result$value)) {
if (is.promising(result$value)) {
# Purposefully NOT including domain (nor replace), as we're already in
# the domain at this point
p <- promise_chain(valueWithVisible(result), ..., catch = catch, finally = finally)
@@ -1576,7 +1576,7 @@ hybrid_chain <- function(expr, ..., catch = NULL, finally = NULL,
}
if (!is.null(domain)) {
promises::with_promise_domain(domain, do(), replace = replace)
with_promise_domain(domain, do(), replace = replace)
} else {
do()
}
@@ -1594,7 +1594,7 @@ createVarPromiseDomain <- function(env, name, value) {
force(name)
force(value)
promises::new_promise_domain(
new_promise_domain(
wrapOnFulfilled = function(onFulfilled) {
function(...) {
orig <- env[[name]]

View File

@@ -37,7 +37,7 @@
15 57 promises::with_promise_domain
16 56 captureStackTraces
17 55 force
18 54 with_context_ospan_async
18 54 with_otel_span_context
19 53 force
20 52 domain$wrapSync
21 51 promises::with_promise_domain
@@ -72,7 +72,7 @@
50 22 promises::with_promise_domain
51 21 captureStackTraces
52 20 force
53 19 with_context_ospan_async
53 19 with_otel_span_context
54 18 force
55 17 domain$wrapSync
56 16 promises::with_promise_domain

View File

@@ -930,7 +930,7 @@ test_that("bindCache reactive visibility - async", {
k <- reactiveVal(0)
res <- NULL
r <- reactive({
promise(function(resolve, reject) {
promises::promise(function(resolve, reject) {
if (k() == 0) resolve(invisible(k()))
else resolve(k())
})
@@ -1140,7 +1140,7 @@ test_that("Custom render functions that call installExprFunction", {
test_that("cacheWriteHook and cacheReadHook for render functions", {
testthat::skip_if(get_tracer()$is_enabled(), "Skipping stack trace tests when OpenTelemetry is already enabled")
testthat::skip_if(shiny_otel_tracer()$is_enabled(), "Skipping stack trace tests when OpenTelemetry is already enabled")
write_hook_n <- 0
read_hook_n <- 0

View File

@@ -54,38 +54,26 @@ test_server_with_otel_error <- function(session, server, expr, sanitize = FALSE,
}
test_that("has_seen_ospan_error() returns FALSE for unseen errors", {
test_that("mark_otel_exception_as_seen() returns modified condition", {
cnd <- simpleError("test error")
expect_false(has_seen_ospan_error(cnd))
})
test_that("set_ospan_error_as_seen() marks error as seen", {
cnd <- simpleError("test error")
expect_false(has_seen_ospan_error(cnd))
cnd <- set_ospan_error_as_seen(cnd)
expect_true(has_seen_ospan_error(cnd))
})
test_that("set_ospan_error_as_seen() returns modified condition", {
cnd <- simpleError("test error")
result <- set_ospan_error_as_seen(cnd)
result <- mark_otel_exception_as_seen(cnd)
expect_true(inherits(result, "error"))
expect_true(inherits(result, "condition"))
expect_equal(conditionMessage(result), "test error")
expect_true(isTRUE(result$.shiny_error_seen))
expect_true(isTRUE(result$.shiny_otel_exception))
})
test_that("has_seen_ospan_error() detects marked errors", {
test_that("mark_otel_exception_as_seen() marks error as seen", {
cnd <- simpleError("test error")
cnd$.shiny_error_seen <- TRUE
expect_false(has_seen_otel_exception(cnd))
expect_true(has_seen_ospan_error(cnd))
cnd <- mark_otel_exception_as_seen(cnd)
expect_true(has_seen_otel_exception(cnd))
})
test_that("set_ospan_error_status() records sanitized errors by default", {
test_that("set_otel_exception_status() records sanitized errors by default", {
server <- function(input, output, session) {
r1 <- reactive(label = "r1", {
stop("test error in r1")
@@ -125,7 +113,7 @@ test_that("set_ospan_error_status() records sanitized errors by default", {
)
})
test_that("set_ospan_error_status() records exception only once in reactive context", {
test_that("set_otel_exception_status() records exception only once in reactive context", {
server <- function(input, output, session) {
r1 <- reactive(label = "r1", {
stop("test error in r1")
@@ -162,7 +150,7 @@ test_that("set_ospan_error_status() records exception only once in reactive cont
)
})
test_that("set_ospan_error_status() records exception for multiple independent errors", {
test_that("set_otel_exception_status() records exception for multiple independent errors", {
server <- function(input, output, session) {
r1 <- reactive(label = "r1", {
stop("error in r1")
@@ -194,7 +182,7 @@ test_that("set_ospan_error_status() records exception for multiple independent e
expect_gte(length(exception_events), 1)
})
test_that("set_ospan_error_status() does not record shiny.custom.error", {
test_that("set_otel_exception_status() does not record shiny.custom.error", {
server <- function(input, output, session) {
r <- reactive(label = "r", {
cnd <- simpleError("custom error")
@@ -218,7 +206,7 @@ test_that("set_ospan_error_status() does not record shiny.custom.error", {
}
})
test_that("set_ospan_error_status() does not record shiny.silent.error", {
test_that("set_otel_exception_status() does not record shiny.silent.error", {
server <- function(input, output, session) {
r <- reactive(label = "r", {
cnd <- simpleError("silent error")

View File

@@ -1,5 +1,5 @@
# Tests for label methods used in otel-bind.R
test_that("ospan_label_reactive generates correct labels", {
test_that("otel_span_label_reactive generates correct labels", {
# Create mock reactive with observable attribute
x_reactive <- reactive({ 42 })
@@ -7,40 +7,40 @@ test_that("ospan_label_reactive generates correct labels", {
x_observe <- observe({ 42 })
# Test without domain
result <- ospan_label_reactive(x_reactive, domain = MockShinySession$new())
result <- otel_span_label_reactive(x_reactive, domain = MockShinySession$new())
expect_equal(result, "reactive mock-session:x_reactive")
# Test with cache class
x_reactive_cache <- bindCache(x_reactive, {"cacheKey"})
result <- ospan_label_reactive(x_reactive_cache, domain = NULL)
result <- otel_span_label_reactive(x_reactive_cache, domain = NULL)
expect_equal(result, "reactive cache x_reactive_cache")
x_reactive_cache <- x_reactive |> bindCache({"cacheKey"})
result <- ospan_label_reactive(x_reactive_cache, domain = NULL)
result <- otel_span_label_reactive(x_reactive_cache, domain = NULL)
expect_equal(result, "reactive cache x_reactive_cache")
x_reactive_cache <- reactive({42}) |> bindCache({"cacheKey"})
result <- ospan_label_reactive(x_reactive_cache, domain = NULL)
result <- otel_span_label_reactive(x_reactive_cache, domain = NULL)
expect_equal(result, "reactive cache x_reactive_cache")
# Test with event class
x_reactive_event <- bindEvent(x_reactive, {"eventKey"})
result <- ospan_label_reactive(x_reactive_event, domain = NULL)
result <- otel_span_label_reactive(x_reactive_event, domain = NULL)
expect_equal(result, "reactive event x_reactive_event")
x_reactive_event <- x_reactive |> bindEvent({"eventKey"})
result <- ospan_label_reactive(x_reactive_event, domain = NULL)
result <- otel_span_label_reactive(x_reactive_event, domain = NULL)
expect_equal(result, "reactive event x_reactive_event")
result <- ospan_label_reactive(x_reactive |> bindEvent({"eventKey"}), domain = NULL)
result <- otel_span_label_reactive(x_reactive |> bindEvent({"eventKey"}), domain = NULL)
expect_equal(result, "reactive event <anonymous>")
x_reactive_event <- reactive({42}) |> bindEvent({"eventKey"})
result <- ospan_label_reactive(x_reactive_event, domain = NULL)
result <- otel_span_label_reactive(x_reactive_event, domain = NULL)
expect_equal(result, "reactive event x_reactive_event")
# x_reactive_both <- bindCache(bindEvent(x_reactive, {"eventKey"}), {"cacheKey"})
# result <- ospan_label_reactive(x_reactive_both, domain = NULL)
# result <- otel_span_label_reactive(x_reactive_both, domain = NULL)
# expect_equal(result, "reactive event cache x_reactive_both")
x_reactive_both2 <- bindEvent(bindCache(x_reactive, {"cacheKey"}), {"eventKey"})
result <- ospan_label_reactive(x_reactive_both2, domain = NULL)
result <- otel_span_label_reactive(x_reactive_both2, domain = NULL)
expect_equal(result, "reactive cache event x_reactive_both2")
})
@@ -62,7 +62,7 @@ test_that("reactive bindCache labels are created", {
"cachedReactive(x_reactive)"
)
expect_equal(
ospan_label_reactive(x_reactive_cache, domain = NULL),
otel_span_label_reactive(x_reactive_cache, domain = NULL),
"reactive cache <anonymous>"
)
})
@@ -106,24 +106,24 @@ test_that("ExtendedTask otel labels are created", {
})
test_that("ospan_label_reactive with pre-defined label", {
test_that("otel_span_label_reactive with pre-defined label", {
x_reactive <- reactive({ 42 }, label = "counter")
result <- ospan_label_reactive(x_reactive, domain = MockShinySession$new())
result <- otel_span_label_reactive(x_reactive, domain = MockShinySession$new())
expect_equal(result, "reactive mock-session:counter")
result <- ospan_label_reactive(x_reactive, domain = NULL)
result <- otel_span_label_reactive(x_reactive, domain = NULL)
expect_equal(result, "reactive counter")
})
test_that("observer labels are preserved", {
x_observe <- observe({ 42 }, label = "my_observer")
expect_equal(x_observe$.label, "my_observer")
expect_equal(ospan_label_observer(x_observe, domain = NULL), "observe my_observer")
expect_equal(otel_span_label_observer(x_observe, domain = NULL), "observe my_observer")
x_observe <- observe({ 42 })
expect_equal(x_observe$.label, "x_observe")
expect_equal(ospan_label_observer(x_observe, domain = NULL), "observe x_observe")
expect_equal(otel_span_label_observer(x_observe, domain = NULL), "observe x_observe")
f <- function() {
observe({ 42 })
@@ -131,35 +131,35 @@ test_that("observer labels are preserved", {
x_observe <- f()
expect_equal(x_observe$.label, as_default_label("observe({\n 42\n})"))
expect_equal(ospan_label_observer(x_observe, domain = NULL), "observe <anonymous>")
expect_equal(otel_span_label_observer(x_observe, domain = NULL), "observe <anonymous>")
})
test_that("ospan_label_observer generates correct labels", {
test_that("otel_span_label_observer generates correct labels", {
x_observe <- observe({ 42 }, label = "test_observer" )
result <- ospan_label_observer(x_observe, domain = MockShinySession$new())
result <- otel_span_label_observer(x_observe, domain = MockShinySession$new())
expect_equal(result, "observe mock-session:test_observer")
result <- ospan_label_observer(x_observe, domain = NULL)
result <- otel_span_label_observer(x_observe, domain = NULL)
expect_equal(result, "observe test_observer")
x_observe_event <- bindEvent(x_observe, {"eventKey"})
result <- ospan_label_observer(x_observe_event, domain = NULL)
result <- otel_span_label_observer(x_observe_event, domain = NULL)
expect_equal(result, "observe event x_observe_event")
x_observe_event <- observe({ 42 }, label = "test_observer" ) |> bindEvent({"eventKey"})
result <- ospan_label_observer(x_observe_event, domain = NULL)
result <- otel_span_label_observer(x_observe_event, domain = NULL)
expect_equal(result, "observe event x_observe_event")
result <- ospan_label_observer(observe({ 42 }, label = "test_observer" ) |> bindEvent({"eventKey"}), domain = NULL)
result <- otel_span_label_observer(observe({ 42 }, label = "test_observer" ) |> bindEvent({"eventKey"}), domain = NULL)
expect_equal(result, "observe event <anonymous>")
x_observe <- observe({ 42 }, label = "test_observer" )
x_observe_event <- x_observe |> bindEvent({"eventKey"})
result <- ospan_label_observer(x_observe_event, domain = NULL)
result <- otel_span_label_observer(x_observe_event, domain = NULL)
expect_equal(result, "observe event x_observe_event")
})
test_that("throttle ospan label is correct", {
test_that("throttle otel span label is correct", {
x_reactive <- reactive({ 42 })
x_throttled1 <- throttle(x_reactive, 1000)
x_throttled2 <- x_reactive |> throttle(1000)
@@ -183,7 +183,7 @@ test_that("throttle ospan label is correct", {
expect_equal(attr(x_throttled3, "observable")$.otelLabel, "throttle x_throttled3")
})
test_that("debounce ospan label is correct", {
test_that("debounce otel span label is correct", {
x_reactive <- reactive({ 42 })
x_debounced1 <- debounce(x_reactive, 1000)
x_debounced2 <- x_reactive |> debounce(1000)
@@ -207,13 +207,13 @@ test_that("debounce ospan label is correct", {
expect_equal(attr(x_debounced3, "observable")$.otelLabel, "debounce x_debounced3")
})
test_that("ospan_label_observer handles module namespacing", {
test_that("otel_span_label_observer handles module namespacing", {
x_observe <- observe({ 42 }, label = "clicks" )
result <- ospan_label_observer(x_observe, domain = MockShinySession$new())
result <- otel_span_label_observer(x_observe, domain = MockShinySession$new())
expect_equal(result, "observe mock-session:clicks")
})
test_that("ospan_label_render_function generates correct labels", {
test_that("otel_span_label_render_function generates correct labels", {
x_render <- renderText({ "Hello" })
mock_domain <- MockShinySession$new()
@@ -223,27 +223,27 @@ test_that("ospan_label_render_function generates correct labels", {
}
)
result <- ospan_label_render_function(x_render, domain = NULL)
result <- otel_span_label_render_function(x_render, domain = NULL)
expect_equal(result, "output plot1")
result <- ospan_label_render_function(x_render, domain = mock_domain)
result <- otel_span_label_render_function(x_render, domain = mock_domain)
expect_equal(result, "output mock-session:plot1")
x_render_event <- bindEvent(x_render, {"eventKey"})
result <- ospan_label_render_function(x_render_event, domain = mock_domain)
result <- otel_span_label_render_function(x_render_event, domain = mock_domain)
expect_equal(result, "output event mock-session:plot1")
x_render_cache <- bindCache(x_render, {"cacheKey"})
result <- ospan_label_render_function(x_render_cache, domain = mock_domain)
result <- otel_span_label_render_function(x_render_cache, domain = mock_domain)
expect_equal(result, "output cache mock-session:plot1")
x_render_both <- bindEvent(bindCache(x_render, {"cacheKey"}), {"eventKey"})
result <- ospan_label_render_function(x_render_both, domain = mock_domain)
result <- otel_span_label_render_function(x_render_both, domain = mock_domain)
expect_equal(result, "output cache event mock-session:plot1")
})
test_that("ospan_label_render_function handles cache and event classes", {
test_that("otel_span_label_render_function handles cache and event classes", {
testthat::local_mocked_bindings(
getCurrentOutputInfo = function(session) {
list(name = "table1")
@@ -256,19 +256,19 @@ test_that("ospan_label_render_function handles cache and event classes", {
x_render_both <- bindEvent(bindCache(x_render, {"cacheKey"}), {"eventKey"})
mock_domain <- MockShinySession$new()
result <- ospan_label_render_function(x_render, domain = NULL)
result <- otel_span_label_render_function(x_render, domain = NULL)
expect_equal(result, "output table1")
result <- ospan_label_render_function(x_render, domain = mock_domain)
result <- otel_span_label_render_function(x_render, domain = mock_domain)
expect_equal(result, "output mock-session:table1")
result <- ospan_label_render_function(x_render_event, domain = mock_domain)
result <- otel_span_label_render_function(x_render_event, domain = mock_domain)
expect_equal(result, "output event mock-session:table1")
result <- ospan_label_render_function(x_render_cache, domain = mock_domain)
result <- otel_span_label_render_function(x_render_cache, domain = mock_domain)
expect_equal(result, "output cache mock-session:table1")
result <- ospan_label_render_function(x_render_both, domain = mock_domain)
result <- otel_span_label_render_function(x_render_both, domain = mock_domain)
expect_equal(result, "output cache event mock-session:table1")
})

View File

@@ -163,7 +163,7 @@ for (bind in c("all", "reactivity")) {
test_that(paste0("bind='", bind, "' extended tasks are supported"), {
server <- function(input, output, session) {
rand_task <- ExtendedTask$new(function() {
promise_resolve(42) |> then(function(value) {
promise_resolve(42) |> promises::then(function(value) {
value
})
})

View File

@@ -1,89 +1,26 @@
# Tests for otel-reactive-update.R functions
# Helper function to create a mock ospan
create_mock_ospan <- function(name, attributes = NULL, ended = FALSE) {
# Helper function to create a mock otel span
create_mock_otel_span <- function(name, attributes = NULL, ended = FALSE) {
structure(
list(name = name, attributes = attributes, ended = ended),
class = "mock_ospan"
class = c("mock_otel_span", "otel_span")
)
}
# Mock is_ospan function
is_ospan <- function(x) {
inherits(x, "mock_ospan") && !isTRUE(x$ended)
}
test_that("has_reactive_ospan_cleanup works correctly", {
test_that("otel_span_reactive_update_init returns early when otel not enabled", {
domain <- MockShinySession$new()
# Initially should be FALSE
expect_false(has_reactive_ospan_cleanup(domain))
# After setting, should be TRUE
domain$userData[["_otel_has_reactive_cleanup"]] <- TRUE
expect_true(has_reactive_ospan_cleanup(domain))
# With FALSE value, should be FALSE
domain$userData[["_otel_has_reactive_cleanup"]] <- FALSE
expect_false(has_reactive_ospan_cleanup(domain))
})
test_that("set_reactive_ospan_cleanup sets flag correctly", {
domain <- MockShinySession$new()
expect_false(has_reactive_ospan_cleanup(domain))
set_reactive_ospan_cleanup(domain)
expect_true(has_reactive_ospan_cleanup(domain))
})
test_that("reactive_update_ospan_is_active works correctly", {
domain <- MockShinySession$new()
# Initially should be FALSE
expect_false(reactive_update_ospan_is_active(domain))
# After setting, should be TRUE
domain$userData[["_otel_reactive_update_is_active"]] <- TRUE
expect_true(reactive_update_ospan_is_active(domain))
# With FALSE value, should be FALSE
domain$userData[["_otel_reactive_update_is_active"]] <- FALSE
expect_false(reactive_update_ospan_is_active(domain))
})
test_that("set_reactive_ospan_is_active sets flag correctly", {
domain <- MockShinySession$new()
expect_false(reactive_update_ospan_is_active(domain))
set_reactive_ospan_is_active(domain)
expect_true(reactive_update_ospan_is_active(domain))
})
test_that("clear_reactive_ospan_is_active clears flag correctly", {
domain <- MockShinySession$new()
# Set the flag first
set_reactive_ospan_is_active(domain)
expect_true(reactive_update_ospan_is_active(domain))
# Clear it
clear_reactive_ospan_is_active(domain)
expect_false(reactive_update_ospan_is_active(domain))
})
test_that("create_reactive_update_ospan returns early when otel not enabled", {
domain <- MockShinySession$new()
# Mock has_otel_bind to return FALSE
# Convince has_otel_bind to return FALSE
withr::local_options(list(shiny.otel.bind = "none"))
# Should return early without creating span
result <- create_reactive_update_ospan(domain = domain)
result <- otel_span_reactive_update_init(domain = domain)
expect_null(result)
expect_null(domain$userData[["_otel_reactive_update_ospan"]])
expect_null(domain$userData[["_otel_span_reactive_update"]])
})
test_that("create_reactive_update_ospan sets up session cleanup on first call", {
test_that("otel_span_reactive_update_init sets up session cleanup on first call", {
callback_added <- FALSE
TestMockShinySession <- R6::R6Class(
"TestMockShinySession",
@@ -100,46 +37,43 @@ test_that("create_reactive_update_ospan sets up session cleanup on first call",
)
domain <- TestMockShinySession$new()
# Mock dependencies
withr::local_options(list(shiny.otel.bind = "reactive_update"))
with_mocked_bindings(
local_mocked_bindings(
has_otel_bind = function(level) level == "reactive_update",
create_shiny_ospan = function(name, ..., attributes = NULL) create_mock_ospan(name, attributes = attributes),
otel_session_id_attrs = function(domain) list(session_id = "mock-session-id"),
{
create_reactive_update_ospan(domain = domain)
start_otel_span = function(name, ..., attributes = NULL) create_mock_otel_span(name, attributes = attributes),
otel_session_id_attrs = function(domain) list(session_id = "mock-session-id")
)
expect_true(callback_added)
expect_true(has_reactive_ospan_cleanup(domain))
expect_equal(domain$userData[["_otel_reactive_update_ospan"]], create_mock_ospan("reactive_update", attributes = list(session_id = "mock-session-id")))
}
otel_span_reactive_update_init(domain = domain)
expect_true(callback_added)
expect_true(domain$userData[["_otel_has_reactive_cleanup"]])
expect_equal(
domain$userData[["_otel_span_reactive_update"]],
create_mock_otel_span("reactive_update", attributes = list(session_id = "mock-session-id"))
)
})
test_that("create_reactive_update_ospan errors when span already exists", {
test_that("otel_span_reactive_update_init errors when span already exists", {
domain <- MockShinySession$new()
domain$token <- "mock-session-token"
# Set up existing span
existing_ospan <- create_mock_ospan("reactive_update", attributes = list(session.id = "mock-session-token"))
domain$userData[["_otel_reactive_update_ospan"]] <- existing_ospan
existing_otel_span <- create_mock_otel_span("reactive_update", attributes = list(session.id = "mock-session-token"))
domain$userData[["_otel_span_reactive_update"]] <- existing_otel_span
# Mock dependencies
with_mocked_bindings(
has_otel_bind = function(level) level == "reactive_update",
is_ospan = function(x) inherits(x, "mock_ospan"),
{
expect_error(
create_reactive_update_ospan(domain = domain),
"Reactive update span already exists"
)
}
local_mocked_bindings(
has_otel_bind = function(level) level == "reactive_update"
)
expect_error(
otel_span_reactive_update_init(domain = domain),
"Reactive update span already exists"
)
})
test_that("create_reactive_update_ospan doesn't setup cleanup twice", {
test_that("otel_span_reactive_update_init doesn't setup cleanup twice", {
TestMockShinySession <- R6::R6Class(
"TestMockShinySession",
inherit = MockShinySession,
@@ -157,89 +91,73 @@ test_that("create_reactive_update_ospan doesn't setup cleanup twice", {
domain <- TestMockShinySession$new()
# Set cleanup flag manually
set_reactive_ospan_cleanup(domain)
domain$userData[["_otel_has_reactive_cleanup"]] <- TRUE
# Mock dependencies
mock_ospan <- create_mock_ospan("reactive_update")
with_mocked_bindings(
local_mocked_bindings(
has_otel_bind = function(level) level == "reactive_update",
create_shiny_ospan = function(...) mock_ospan,
{
create_reactive_update_ospan(domain = domain)
# Should not have called onSessionEnded since cleanup was already set
expect_equal(domain$callback_count, 0)
}
start_otel_span = function(...) create_mock_otel_span("reactive_update")
)
otel_span_reactive_update_init(domain = domain)
# Should not have called onSessionEnded since cleanup was already set
expect_equal(domain$callback_count, 0)
})
test_that("end_reactive_update_ospan ends span when it exists", {
test_that("otel_span_reactive_update_teardown ends span when it exists", {
domain <- MockShinySession$new()
mock_ospan <- create_mock_ospan("reactive_update")
domain$userData[["_otel_reactive_update_ospan"]] <- mock_ospan
mock_otel_span <- create_mock_otel_span("reactive_update")
domain$userData[["_otel_span_reactive_update"]] <- mock_otel_span
span_ended <- FALSE
with_mocked_bindings(
local_mocked_bindings(
end_span = function(span) {
span_ended <<- TRUE
expect_equal(span, mock_ospan)
expect_equal(span, mock_otel_span)
},
.package = "otel",
{
with_mocked_bindings(
is_ospan = function(x) inherits(x, "mock_ospan") && !isTRUE(x$ended),
{
end_reactive_update_ospan(domain = domain)
expect_true(span_ended)
expect_null(domain$userData[["_otel_reactive_update_ospan"]])
}
)
}
.package = "otel"
)
otel_span_reactive_update_teardown(domain = domain)
expect_true(span_ended)
expect_null(domain$userData[["_otel_span_reactive_update"]])
})
test_that("end_reactive_update_ospan handles missing span gracefully", {
test_that("otel_span_reactive_update_teardown handles missing span gracefully", {
domain <- MockShinySession$new()
# No span exists
expect_null(domain$userData[["_otel_reactive_update_ospan"]])
expect_null(domain$userData[["_otel_span_reactive_update"]])
with_mocked_bindings(
is_ospan = function(x) FALSE,
{
# Should not error
expect_no_error(end_reactive_update_ospan(domain = domain))
}
)
# Should not error
expect_no_error(otel_span_reactive_update_teardown(domain = domain))
})
test_that("with_reactive_update_active_ospan executes expr without span", {
test_that("with_otel_span_reactive_update executes expr without span", {
domain <- MockShinySession$new()
# No span exists
test_value <- "initial"
with_mocked_bindings(
is_ospan = function(x) FALSE,
{
result <- with_reactive_update_active_ospan({
test_value <- "modified"
"result_value"
}, domain = domain)
expect_equal(result, "result_value")
expect_equal(test_value, "modified")
}
local_mocked_bindings(
is_otel_span = function(x) FALSE
)
result <- with_otel_span_reactive_update({
test_value <- "modified"
"result_value"
}, domain = domain)
expect_equal(result, "result_value")
expect_equal(test_value, "modified")
})
test_that("with_reactive_update_active_ospan executes expr with active span", {
test_that("with_otel_span_reactive_update executes expr with active span", {
domain <- MockShinySession$new()
mock_ospan <- create_mock_ospan("reactive_update")
domain$userData[["_otel_reactive_update_ospan"]] <- mock_ospan
mock_otel_span <- create_mock_otel_span("reactive_update")
domain$userData[["_otel_span_reactive_update"]] <- mock_otel_span
span_was_active <- FALSE
test_value <- "initial"
@@ -247,16 +165,13 @@ test_that("with_reactive_update_active_ospan executes expr with active span", {
local_mocked_bindings(
with_active_span = function(span, expr) {
span_was_active <<- TRUE
expect_equal(span, mock_ospan)
expect_equal(span, mock_otel_span)
force(expr)
},
.package = "otel"
)
local_mocked_bindings(
is_ospan = function(x) inherits(x, "mock_ospan") && !isTRUE(x$ended)
)
result <- with_reactive_update_active_ospan({
result <- with_otel_span_reactive_update({
test_value <- "modified"
"result_value"
}, domain = domain)
@@ -281,16 +196,14 @@ test_that("session cleanup callback works correctly", {
)
)
domain <- TestMockShinySession$new()
# Mock dependencies and create span with cleanup
mock_ospan <- create_mock_ospan("reactive_update")
mock_otel_span <- create_mock_otel_span("reactive_update")
with_mocked_bindings(
has_otel_bind = function(level) level == "reactive_update",
create_shiny_ospan = function(...) mock_ospan,
start_otel_span = function(...) mock_otel_span,
otel_session_id_attrs = function(domain) list(session_id = "test"),
{
create_reactive_update_ospan(domain = domain)
otel_span_reactive_update_init(domain = domain)
}
)
@@ -298,14 +211,13 @@ test_that("session cleanup callback works correctly", {
expect_true(is.function(domain$cleanup_callback))
# Set up span and test cleanup
domain$userData[["_otel_reactive_update_ospan"]] <- mock_ospan
set_reactive_ospan_cleanup(domain)
domain$userData[["_otel_span_reactive_update"]] <- mock_otel_span
domain$userData[["_otel_has_reactive_cleanup"]] <- TRUE
span_ended <- FALSE
with_mocked_bindings(
has_reactive_ospan_cleanup = function(d) identical(d, domain),
end_reactive_update_ospan = function(domain = NULL) {
otel_span_reactive_update_teardown = function(domain = NULL) {
span_ended <<- TRUE
},
{

View File

@@ -40,14 +40,14 @@ create_mock_session_domain <- function(
domain
}
test_that("use_session_start_ospan_async returns early when otel not enabled", {
test_that("otel_span_session_start returns early when otel not enabled", {
domain <- create_mock_session_domain()
test_value <- "initial"
# Mock has_otel_bind to return FALSE
withr::local_options(list(shiny.otel.bind = "none"))
result <- use_session_start_ospan_async({
result <- otel_span_session_start({
test_value <- "modified"
"result_value"
}, domain = domain)
@@ -58,7 +58,7 @@ test_that("use_session_start_ospan_async returns early when otel not enabled", {
expect_length(domain$cleanup_callbacks, 0)
})
test_that("use_session_start_ospan_async sets up session end callback", {
test_that("otel_span_session_start sets up session end callback", {
domain <- create_mock_session_domain(
token = "session-456",
request = list(PATH_INFO = "/app", HTTP_HOST = "localhost")
@@ -78,7 +78,7 @@ test_that("use_session_start_ospan_async sets up session end callback", {
has_otel_bind = function(level) level == "session",
otel_session_id_attrs = function(domain) list(session.id = domain$token),
otel_session_attrs = function(domain) list(PATH_INFO = "/app"),
with_shiny_ospan_async = function(name, expr, attributes = NULL) {
with_otel_span = function(name, expr, attributes = NULL) {
expect_equal(name, "session_start")
expect_true("session.id" %in% names(attributes))
expect_equal(attributes[["session.id"]], "session-456")
@@ -88,7 +88,7 @@ test_that("use_session_start_ospan_async sets up session end callback", {
expect_length(domain$cleanup_callbacks, 0)
result <- use_session_start_ospan_async({
result <- otel_span_session_start({
test_value <- "modified"
"result_value"
}, domain = domain)
@@ -101,14 +101,14 @@ test_that("use_session_start_ospan_async sets up session end callback", {
)
})
test_that("with_session_end_ospan_async returns early when otel not enabled", {
test_that("otel_span_session_end returns early when otel not enabled", {
domain <- create_mock_session_domain()
test_value <- "initial"
# Mock has_otel_bind to return FALSE
withr::local_options(list(shiny.otel.bind = "none"))
result <- with_session_end_ospan_async({
result <- otel_span_session_end({
test_value <- "modified"
"result_value"
}, domain = domain)
@@ -117,7 +117,7 @@ test_that("with_session_end_ospan_async returns early when otel not enabled", {
expect_equal(test_value, "modified")
})
test_that("with_session_end_ospan_async creates span when enabled", {
test_that("otel_span_session_end creates span when enabled", {
domain <- create_mock_session_domain(token = "session-end-test")
span_created <- FALSE
@@ -129,14 +129,14 @@ test_that("with_session_end_ospan_async creates span when enabled", {
with_mocked_bindings(
has_otel_bind = function(level) level == "session",
otel_session_id_attrs = function(domain) list(session.id = domain$token),
with_shiny_ospan_async = function(name, expr, attributes = NULL) {
with_otel_span = function(name, expr, attributes = NULL) {
span_created <<- TRUE
expect_equal(name, "session_end")
expect_equal(attributes[["session.id"]], "session-end-test")
force(expr)
},
{
result <- with_session_end_ospan_async({
result <- otel_span_session_end({
test_value <- "modified"
"result_value"
}, domain = domain)
@@ -161,10 +161,10 @@ test_that("otel_session_attrs extracts request attributes correctly", {
attrs <- otel_session_attrs(domain)
expect_equal(attrs$PATH_INFO, "/myapp/page")
expect_equal(attrs$HTTP_HOST, "example.com")
expect_equal(attrs$HTTP_ORIGIN, "https://example.com")
expect_equal(attrs$SERVER_PORT, 8080L) # Should be converted to integer
expect_equal(attrs$server.path, "/myapp/page")
expect_equal(attrs$server.address, "example.com")
expect_equal(attrs$server.origin, "https://example.com")
expect_equal(attrs$server.port, 8080L) # Should be converted to integer
})
test_that("otel_session_attrs handles websocket PATH_INFO", {
@@ -178,7 +178,7 @@ test_that("otel_session_attrs handles websocket PATH_INFO", {
attrs <- otel_session_attrs(domain)
# Should strip websocket suffix
expect_equal(attrs$PATH_INFO, "/myapp/")
expect_equal(attrs$server.path, "/myapp/")
})
test_that("otel_session_attrs handles missing request fields", {
@@ -191,10 +191,10 @@ test_that("otel_session_attrs handles missing request fields", {
attrs <- otel_session_attrs(domain)
expect_equal(attrs$PATH_INFO, "")
expect_equal(attrs$HTTP_HOST, "localhost")
expect_equal(attrs$HTTP_ORIGIN, "")
expect_equal(attrs$SERVER_PORT, NA_integer_)
expect_equal(attrs$server.path, "")
expect_equal(attrs$server.address, "localhost")
expect_equal(attrs$server.origin, "")
expect_equal(attrs$server.port, NA_integer_)
})
test_that("otel_session_attrs handles empty request", {
@@ -202,10 +202,10 @@ test_that("otel_session_attrs handles empty request", {
attrs <- otel_session_attrs(domain)
expect_equal(attrs$PATH_INFO, "")
expect_equal(attrs$HTTP_HOST, "")
expect_equal(attrs$HTTP_ORIGIN, "")
expect_equal(attrs$SERVER_PORT, NA_integer_)
expect_equal(attrs$server.path, "")
expect_equal(attrs$server.address, "")
expect_equal(attrs$server.origin, "")
expect_equal(attrs$server.port, NA_integer_)
})
test_that("otel_session_attrs handles invalid SERVER_PORT gracefully", {
@@ -217,7 +217,7 @@ test_that("otel_session_attrs handles invalid SERVER_PORT gracefully", {
attrs <- otel_session_attrs(domain)
# Should remain as string if conversion fails
expect_equal(attrs$SERVER_PORT, "invalid")
expect_equal(attrs$server.port, "invalid")
})
test_that("otel_session_id_attrs returns correct session ID", {
@@ -263,7 +263,7 @@ test_that("integration test - session start with full request", {
has_otel_bind = function(level) level == "session",
otel_session_id_attrs = otel_session_id_attrs, # Use real function
otel_session_attrs = otel_session_attrs, # Use real function
with_shiny_ospan_async = function(name, expr, attributes = NULL) {
with_otel_span = function(name, expr, attributes = NULL) {
span_attributes <<- attributes
force(expr)
},
@@ -272,7 +272,7 @@ test_that("integration test - session start with full request", {
expect_length(domain$cleanup_callbacks, 0)
result <- use_session_start_ospan_async({
result <- otel_span_session_start({
"test_result"
}, domain = domain)
@@ -280,9 +280,9 @@ test_that("integration test - session start with full request", {
# Check span attributes include both session ID and request info
expect_equal(span_attributes[["session.id"]], "integration-test-session")
expect_equal(span_attributes[["PATH_INFO"]], "/dashboard/")
expect_equal(span_attributes[["HTTP_HOST"]], "shiny.example.com")
expect_equal(span_attributes[["SERVER_PORT"]], 3838L)
expect_equal(span_attributes[["server.path"]], "/dashboard/")
expect_equal(span_attributes[["server.address"]], "shiny.example.com")
expect_equal(span_attributes[["server.port"]], 3838L)
}
)
})

View File

@@ -1,9 +1,13 @@
# Tests for otel-shiny.R functions
# Helper function to create a mock otel span
create_mock_otel_span <- function() {
create_mock_otel_span <- function(name = "test_span") {
structure(
list(name = "test_span"),
list(
name = name,
activate = function(...) NULL,
end = function(...) NULL
),
class = "otel_span"
)
}
@@ -11,7 +15,11 @@ create_mock_otel_span <- function() {
# Helper function to create a mock tracer
create_mock_tracer <- function() {
structure(
list(name = "mock_tracer", is_enabled = function() TRUE),
list(
name = "mock_tracer",
is_enabled = function() TRUE,
start_span = function(name, ...) create_mock_otel_span(name)
),
class = "otel_tracer"
)
}
@@ -28,38 +36,8 @@ test_that("otel_tracer_name constant is correct", {
expect_equal(otel_tracer_name, "co.posit.r-package.shiny")
})
test_that("with_shiny_ospan_async calls with_ospan_async with correct parameters", {
mock_tracer <- create_mock_tracer()
with_ospan_async_called <- FALSE
test_value <- "initial"
with_mocked_bindings(
get_tracer = function() mock_tracer,
with_ospan_async = function(name, expr, ..., attributes = NULL, tracer = NULL) {
with_ospan_async_called <<- TRUE
expect_equal(name, "test_span")
expect_equal(tracer, mock_tracer)
expect_equal(attributes, list(key = "value"))
force(expr)
},
{
result <- with_shiny_ospan_async(
"test_span",
{
test_value <- "modified"
"result_value"
},
attributes = list(key = "value")
)
expect_true(with_ospan_async_called)
expect_equal(result, "result_value")
expect_equal(test_value, "modified")
}
)
})
test_that("create_shiny_ospan calls otel::start_span with correct parameters", {
test_that("start_otel_span calls otel::start_span with correct parameters", {
mock_tracer <- create_mock_tracer()
mock_span <- create_mock_otel_span()
start_span_called <- FALSE
@@ -73,49 +51,51 @@ test_that("create_shiny_ospan calls otel::start_span with correct parameters", {
},
.package = "otel"
)
with_mocked_bindings(
get_tracer = function() mock_tracer,
{
result <- create_shiny_ospan("test_span", extra_param = "value")
expect_true(start_span_called)
expect_equal(result, mock_span)
}
local_mocked_bindings(
shiny_otel_tracer = function() mock_tracer,
)
result <- start_otel_span("test_span", extra_param = "value")
expect_true(start_span_called)
expect_equal(result, mock_span)
})
test_that("is_ospan correctly identifies otel spans", {
test_that("is_otel_span correctly identifies otel spans", {
# Test with otel_span object
otel_span <- create_mock_otel_span()
expect_true(is_ospan(otel_span))
expect_true(is_otel_span(otel_span))
# Test with non-otel objects
expect_false(is_ospan("string"))
expect_false(is_ospan(123))
expect_false(is_ospan(list()))
expect_false(is_ospan(NULL))
expect_false(is_otel_span("string"))
expect_false(is_otel_span(123))
expect_false(is_otel_span(list()))
expect_false(is_otel_span(NULL))
# Test with object that has different class
other_obj <- structure(list(), class = "other_class")
expect_false(is_ospan(other_obj))
expect_false(is_otel_span(other_obj))
})
test_that("testthat__is_testing detects testing environment", {
# Test when TESTTHAT env var is set to "true"
withr::local_envvar(list(TESTTHAT = "true"))
expect_true(testthat__is_testing())
withr::with_envvar(list(TESTTHAT = "true"), {
expect_true(testthat__is_testing())
})
# Test when TESTTHAT env var is not set
withr::local_envvar(list(TESTTHAT = NA))
expect_false(testthat__is_testing())
withr::with_envvar(list(TESTTHAT = NA), {
expect_false(testthat__is_testing())
})
# Test when TESTTHAT env var is set to other values
withr::local_envvar(list(TESTTHAT = "false"))
expect_false(testthat__is_testing())
withr::with_envvar(list(TESTTHAT = "false"), {
expect_false(testthat__is_testing())
})
withr::local_envvar(list(TESTTHAT = ""))
expect_false(testthat__is_testing())
withr::with_envvar(list(TESTTHAT = ""), {
expect_false(testthat__is_testing())
})
})
test_that("otel_log calls otel::log with correct parameters", {
@@ -131,14 +111,12 @@ test_that("otel_log calls otel::log with correct parameters", {
},
.package = "otel"
)
with_mocked_bindings(
get_ospan_logger = function() mock_logger,
{
otel_log("test message", severity = "warn")
expect_true(log_called)
}
local_mocked_bindings(
shiny_otel_logger = function() mock_logger,
)
otel_log("test message", severity = "warn")
expect_true(log_called)
})
test_that("otel_log uses default severity and logger", {
@@ -154,14 +132,12 @@ test_that("otel_log uses default severity and logger", {
},
.package = "otel"
)
with_mocked_bindings(
get_ospan_logger = function() mock_logger,
{
otel_log("default test")
expect_true(log_called)
}
local_mocked_bindings(
shiny_otel_logger = function() mock_logger,
)
otel_log("default test")
expect_true(log_called)
})
test_that("otel_is_tracing_enabled calls otel::is_tracing_enabled", {
@@ -176,15 +152,13 @@ test_that("otel_is_tracing_enabled calls otel::is_tracing_enabled", {
},
.package = "otel"
)
with_mocked_bindings(
get_tracer = function() mock_tracer,
{
result <- otel_is_tracing_enabled()
expect_true(is_tracing_called)
expect_true(result)
}
local_mocked_bindings(
shiny_otel_tracer = function() mock_tracer,
)
result <- otel_is_tracing_enabled()
expect_true(is_tracing_called)
expect_true(result)
})
test_that("otel_is_tracing_enabled accepts custom tracer", {
@@ -205,32 +179,33 @@ test_that("otel_is_tracing_enabled accepts custom tracer", {
expect_false(result)
})
test_that("get_ospan_logger caches logger in non-test environment", {
test_that("shiny_otel_logger caches logger in non-test environment", {
mock_logger <- create_mock_logger()
get_logger_call_count <- 0
fn_env <- environment(get_ospan_logger)
fn_env <- environment(shiny_otel_logger)
# Reset cached logger now and when test ends
fn_env$reset_logger()
withr::defer({ fn_env$reset_logger() })
local_mocked_bindings(
otel_get_logger = function() {
get_logger = function(...) {
get_logger_call_count <<- get_logger_call_count + 1
mock_logger
}
},
.package = "otel"
)
with_mocked_bindings(
testthat__is_testing = function() TRUE,
{
# First call
logger1 <- get_ospan_logger()
logger1 <- shiny_otel_logger()
expect_equal(logger1, mock_logger)
expect_equal(get_logger_call_count, 1)
# Second call should call otel::get_logger again (no caching in tests)
logger2 <- get_ospan_logger()
logger2 <- shiny_otel_logger()
expect_equal(logger2, mock_logger)
expect_equal(get_logger_call_count, 2) # Incremented
}
@@ -240,12 +215,12 @@ test_that("get_ospan_logger caches logger in non-test environment", {
testthat__is_testing = function() FALSE,
{
# First call should call otel::get_logger
logger1 <- get_ospan_logger()
logger1 <- shiny_otel_logger()
expect_equal(logger1, mock_logger)
expect_equal(get_logger_call_count, 3)
# Second call should use cached logger
logger2 <- get_ospan_logger()
logger2 <- shiny_otel_logger()
expect_equal(logger2, mock_logger)
expect_equal(get_logger_call_count, 3) # Still 3, not incremented
}
@@ -253,32 +228,33 @@ test_that("get_ospan_logger caches logger in non-test environment", {
})
test_that("get_tracer caches tracer in non-test environment", {
test_that("shiny_otel_tracer caches tracer in non-test environment", {
mock_tracer <- create_mock_tracer()
get_tracer_call_count <- 0
fn_env <- environment(get_tracer)
fn_env <- environment(shiny_otel_tracer)
# Reset cached tracer now and when test ends
fn_env$reset_tracer()
withr::defer({ fn_env$reset_tracer() })
local_mocked_bindings(
otel_get_tracer = function() {
get_tracer = function(...) {
get_tracer_call_count <<- get_tracer_call_count + 1
mock_tracer
}
},
.package = "otel"
)
with_mocked_bindings(
testthat__is_testing = function() TRUE,
{
# First call
tracer1 <- get_tracer()
tracer1 <- shiny_otel_tracer()
expect_equal(tracer1, mock_tracer)
expect_equal(get_tracer_call_count, 1)
# Second call should call otel::get_tracer again (no caching in tests)
tracer2 <- get_tracer()
tracer2 <- shiny_otel_tracer()
expect_equal(tracer2, mock_tracer)
expect_equal(get_tracer_call_count, 2) # Incremented
}
@@ -288,59 +264,48 @@ test_that("get_tracer caches tracer in non-test environment", {
testthat__is_testing = function() FALSE,
{
# First call should call otel::get_tracer
tracer1 <- get_tracer()
tracer1 <- shiny_otel_tracer()
expect_equal(tracer1, mock_tracer)
expect_equal(get_tracer_call_count, 3)
# Second call should use cached tracer
tracer2 <- get_tracer()
tracer2 <- shiny_otel_tracer()
expect_equal(tracer2, mock_tracer)
expect_equal(get_tracer_call_count, 3) # Still 3, not incremented
}
)
})
test_that("integration test - with_shiny_ospan_async uses cached tracer", {
test_that("integration test - with_otel_span uses cached tracer", {
mock_tracer <- create_mock_tracer()
get_tracer_call_count <- 0
with_ospan_async_called <- FALSE
fn_env <- environment(get_tracer)
fn_env <- environment(shiny_otel_tracer)
# Reset cached tracer now and when test ends
fn_env$reset_tracer()
withr::defer({ fn_env$reset_tracer() })
local_mocked_bindings(
otel_get_tracer = function() {
get_tracer = function() {
get_tracer_call_count <<- get_tracer_call_count + 1
mock_tracer
}
)
with_mocked_bindings(
testthat__is_testing = function() FALSE,
with_ospan_async = function(name, expr, ..., attributes = NULL, tracer = NULL) {
with_ospan_async_called <<- TRUE
expect_equal(tracer, mock_tracer)
force(expr)
},
{
# First call to with_shiny_ospan_async
with_shiny_ospan_async("span1", { "result1" })
expect_equal(get_tracer_call_count, 1)
expect_true(with_ospan_async_called)
with_ospan_async_called <- FALSE
# Second call should use cached tracer
with_shiny_ospan_async("span2", { "result2" })
expect_equal(get_tracer_call_count, 1) # Still 1, tracer was cached
expect_true(with_ospan_async_called)
}
.package = "otel"
)
local_mocked_bindings(
testthat__is_testing = function() FALSE,
)
# First call to with_otel_span
with_otel_span("span1", { "result1" })
expect_equal(get_tracer_call_count, 1)
# Second call should use cached tracer
with_otel_span("span2", { "result2" })
expect_equal(get_tracer_call_count, 1) # Still 1, tracer was cached
})
test_that("integration test - create_shiny_ospan with custom parameters", {
test_that("integration test - start_otel_span with custom parameters", {
mock_tracer <- create_mock_tracer()
mock_span <- create_mock_otel_span()
start_span_params <- list()
@@ -356,21 +321,19 @@ test_that("integration test - create_shiny_ospan with custom parameters", {
},
.package = "otel"
)
with_mocked_bindings(
get_tracer = function() mock_tracer,
{
result <- create_shiny_ospan(
"custom_span",
attributes = list(key = "value"),
parent = "parent_span"
)
expect_equal(result, mock_span)
expect_equal(start_span_params$name, "custom_span")
expect_equal(start_span_params$tracer, mock_tracer)
expect_equal(start_span_params$extra_args$attributes, list(key = "value"))
expect_equal(start_span_params$extra_args$parent, "parent_span")
}
local_mocked_bindings(
shiny_otel_tracer = function() mock_tracer,
)
result <- start_otel_span(
"custom_span",
attributes = list(key = "value"),
parent = "parent_span"
)
expect_equal(result, mock_span)
expect_equal(start_span_params$name, "custom_span")
expect_equal(start_span_params$tracer, mock_tracer)
expect_equal(start_span_params$extra_args$attributes, list(key = "value"))
expect_equal(start_span_params$extra_args$parent, "parent_span")
})

View File

@@ -1,6 +1,6 @@
with_several_promise_domains <- function(expr) {
withReactiveDomain(MockShinySession$new(), {
promises::with_promise_domain(reactivePromiseDomain(), {
with_promise_domain(reactivePromiseDomain(), {
captureStackTraces({
expr
})

View File

@@ -118,7 +118,7 @@ dumpTests <- function(df) {
}
test_that("integration tests", {
if (get_tracer()$is_enabled()) {
if (shiny_otel_tracer()$is_enabled()) {
announce_snapshot_file(name = "stacks.md")
skip("Skipping stack trace tests when OpenTelemetry is already enabled")

View File

@@ -1,6 +1,3 @@
library(shiny)
library(testthat)
test_that("testServer works with dir app", {
# app.R
testServer(test_path("..", "test-modules", "06_tabsets"), {

View File

@@ -1,6 +1,3 @@
library(shiny)
library(testthat)
test_that("Nested modules", {
child <- function(id) {
moduleServer(id, function(input, output, session) {

View File

@@ -1,6 +1,3 @@
library(shiny)
library(testthat)
test_that("Variables outside of the module are inaccessible", {
module <- local({
outside <- 123

View File

@@ -1,7 +1,5 @@
library(shiny)
library(testthat)
skip_if_not_installed("future")
library(future, warn.conflicts = FALSE)
library(promises)
test_that("handles observers", {
server <- function(input, output, session) {
@@ -746,7 +744,7 @@ test_that("promise chains evaluate in correct order", {
server <- function(input, output, session) {
r1 <- reactive({
promise(function(resolve, reject) {
promises::promise(function(resolve, reject) {
pushMessage("promise 1")
resolve(input$go)
})$then(function(value) {
@@ -755,7 +753,7 @@ test_that("promise chains evaluate in correct order", {
})
})
r2 <- reactive({
promise(function(resolve, reject) {
promises::promise(function(resolve, reject) {
pushMessage("promise 2")
resolve(input$go)
})$then(function(value) {