Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
e1b9056
Consolidate code
schloerke Oct 14, 2025
fcc631f
Add tests for otel_bind_is_enabled function
schloerke Oct 14, 2025
8899931
Add tests for as_otel_bind validation and errors
schloerke Oct 14, 2025
46c207e
Test and add labels for all reactive expressions, including bindCache…
schloerke Oct 15, 2025
1d7eec7
Split test file
schloerke Oct 15, 2025
f4ebbe5
Create test-otel-reactive-update.R
schloerke Oct 15, 2025
6d60024
Add unit tests for `otel-session.R`; Safely convert SERVER_PORT to in…
schloerke Oct 15, 2025
b72e031
Rename `otel.R` -> `otel-shiny.R`
schloerke Oct 15, 2025
91b7b08
Test otel-shiny.R functions
schloerke Oct 15, 2025
e742002
Fix broken tests
schloerke Oct 16, 2025
ada4f76
Skip stack trace tests if OpenTelemetry is enabled
schloerke Oct 17, 2025
3d2c00a
Fix bad cache reset for otel tracer / loggers
schloerke Oct 17, 2025
2968364
Create test-otel-attr-srcref.R
schloerke Oct 17, 2025
ce3c0e5
Add otel attr support to render functions and extended tasks
schloerke Oct 17, 2025
73d90c9
Update extended-task.R
schloerke Oct 17, 2025
c16b06f
Do not bind to mock session output observer (let the render function …
schloerke Oct 17, 2025
4f9ba63
Test extended task otel labels
schloerke Oct 17, 2025
9eb62a3
Use otelsdk::with_otel_record and testServer() to test ospan nesting …
schloerke Oct 17, 2025
3ca2795
Merge branch 'main' into otel-testing
schloerke Oct 22, 2025
8498b8b
Merge branch 'otel-testing' of https://github.com/rstudio/shiny into …
schloerke Oct 22, 2025
b477ffd
Update CRAN promises 1.4.0
schloerke Oct 22, 2025
1203b51
Fix namespace issue
schloerke Oct 22, 2025
53cacdb
Add better default label handling for any function, not just the spec…
schloerke Oct 22, 2025
1dc96f9
Remove `session.end` log as `session_end` span is good enough
schloerke Oct 22, 2025
7b547e9
Add more labels and otel src refs
schloerke Oct 23, 2025
96d3f5a
add debounce and throttle otel src refs
schloerke Oct 23, 2025
caf6677
Update test-otel-session.R
schloerke Oct 23, 2025
368f9d7
Update test-otel-mock.R
schloerke Oct 23, 2025
2c017a2
Test the mock session with many values of `bind`
schloerke Oct 23, 2025
84251f8
Remove `QUERY_STRING` from `otel_session_attrs()` as the websocket do…
schloerke Oct 23, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 2 additions & 5 deletions DESCRIPTION
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ Imports:
lifecycle (>= 0.2.0),
mime (>= 0.3),
otel,
promises (>= 1.3.3.9006),
promises (>= 1.4.0),
R6 (>= 2.0),
rlang (>= 0.4.10),
sourcetools,
Expand Down Expand Up @@ -120,8 +120,6 @@ Suggests:
testthat (>= 3.2.1),
watcher,
yaml
Remotes:
rstudio/promises
Config/Needs/check: shinytest2
Config/testthat/edition: 3
Encoding: UTF-8
Expand Down Expand Up @@ -190,8 +188,7 @@ Collate:
'otel-label.R'
'otel-reactive-update.R'
'otel-session.R'
'otel-with.R'
'otel.R'
'otel-shiny.R'
'priorityqueue.R'
'progress.R'
'react.R'
Expand Down
1 change: 1 addition & 0 deletions NAMESPACE
Original file line number Diff line number Diff line change
Expand Up @@ -392,6 +392,7 @@ importFrom(promises,local_ospan_promise_domain)
importFrom(promises,promise)
importFrom(promises,promise_reject)
importFrom(promises,promise_resolve)
importFrom(promises,then)
importFrom(promises,with_ospan_async)
importFrom(promises,with_ospan_promise_domain)
importFrom(rlang,"%||%")
Expand Down
5 changes: 4 additions & 1 deletion NEWS.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,10 @@

* Fixed an issue where `updateSelectizeInput(options = list(plugins="remove_button"))` could lead to multiple remove buttons. (#4275)

* The default label for `reactiveValues()`, `reactivePoll()`, `reactiveFileReader()`, `debounce()`, and `throttle()` will now attempt to retrieve the assigned name if the srcref is available. If a value can not easily be produced, a default label will be used instead. (#4269)
* The default label for items described below will now attempt to retrieve the assigned name if the srcref is available. If a value can not easily be produced, a default label will be used instead. This should improve the OpenTelemetry span labels and the reactlog experience. (#4269, #4300)
* `reactiveValues()`, `reactivePoll()`, `reactiveFileReader()`, `debounce()`, `throttle()`, `observe()`
* Combinations of `bindEvent()` and `reactive()` / `observe()`
* Combination of `bindCache()` and `reactive()`

## Changes

Expand Down
38 changes: 29 additions & 9 deletions R/bind-cache.R
Original file line number Diff line number Diff line change
Expand Up @@ -478,7 +478,12 @@ bindCache.default <- function(x, ...) {
bindCache.reactiveExpr <- function(x, ..., cache = "app") {
check_dots_unnamed()

label <- exprToLabel(substitute(x), "cachedReactive")
call_srcref <- attr(sys.call(-1), "srcref", exact = TRUE)
label <- rassignSrcrefToLabel(
call_srcref,
defaultLabel = exprToLabel(substitute(x), "cachedReactive")
)

domain <- reactive_get_domain(x)

# Convert the ... to a function that returns their evaluated values.
Expand All @@ -490,6 +495,9 @@ bindCache.reactiveExpr <- function(x, ..., cache = "app") {
cacheHint <- rlang::hash(extractCacheHint(x))
valueFunc <- wrapFunctionLabel(valueFunc, "cachedReactiveValueFunc", ..stacktraceon = TRUE)

x_classes <- class(x)
x_otel_attrs <- attr(x, "observable", exact = TRUE)$.otelAttrs

# Don't hold on to the reference for x, so that it can be GC'd
rm(x)
# Hacky workaround for issue with `%>%` preventing GC:
Expand All @@ -498,16 +506,27 @@ bindCache.reactiveExpr <- function(x, ..., cache = "app") {
rm(list = ".", envir = .GenericCallEnv, inherits = FALSE)
}


res <- reactive(label = label, domain = domain, {
cache <- resolve_cache_object(cache, domain)
hybrid_chain(
keyFunc(),
generateCacheFun(valueFunc, cache, cacheHint, cacheReadHook = identity, cacheWriteHook = identity)
)
with_no_otel_bind({
res <- reactive(label = label, domain = domain, {
cache <- resolve_cache_object(cache, domain)
hybrid_chain(
keyFunc(),
generateCacheFun(valueFunc, cache, cacheHint, cacheReadHook = identity, cacheWriteHook = identity)
)
})
})

class(res) <- c("reactive.cache", class(res))

impl <- attr(res, "observable", exact = TRUE)
impl$.otelAttrs <- x_otel_attrs
if (!is.null(call_srcref)) {
otelAttrs <- otel_srcref_attributes(call_srcref)
impl$.otelAttrs[names(otelAttrs)] <- otelAttrs
}
if (has_otel_bind("reactivity")) {
res <- bind_otel_reactive_expr(res)
}
res
}

Expand All @@ -534,6 +553,7 @@ bindCache.shiny.render.function <- function(x, ..., cache = "app") {
)
}

# Passes over the otelAttrs from valueFunc to renderFunc
renderFunc <- addAttributes(renderFunc, renderFunctionAttributes(valueFunc))
class(renderFunc) <- c("shiny.render.function.cache", class(valueFunc))
renderFunc
Expand Down Expand Up @@ -585,7 +605,7 @@ bindCache.shiny.renderPlot <- function(x, ...,

observe({
doResizeCheck()
})
}, label = "plot-resize")
# TODO: Make sure this observer gets GC'd if output$foo is replaced.
# Currently, if you reassign output$foo, the observer persists until the
# session ends. This is generally bad programming practice and should be
Expand Down
43 changes: 40 additions & 3 deletions R/bind-event.R
Original file line number Diff line number Diff line change
Expand Up @@ -196,10 +196,20 @@ bindEvent.reactiveExpr <- function(x, ..., ignoreNULL = TRUE, ignoreInit = FALSE
valueFunc <- reactive_get_value_func(x)
valueFunc <- wrapFunctionLabel(valueFunc, "eventReactiveValueFunc", ..stacktraceon = TRUE)

label <- label %||%
sprintf('bindEvent(%s, %s)', attr(x, "observable", exact = TRUE)$.label, quos_to_label(qs))
call_srcref <- attr(sys.call(-1), "srcref", exact = TRUE)
if (is.null(label)) {
label <- rassignSrcrefToLabel(
call_srcref,
defaultLabel = as_default_label(sprintf(
'bindEvent(%s, %s)',
attr(x, "observable", exact = TRUE)$.label,
quos_to_label(qs)
))
)
}

x_classes <- class(x)
x_otel_attrs <- attr(x, "observable", exact = TRUE)$.otelAttrs

# Don't hold on to the reference for x, so that it can be GC'd
rm(x)
Expand Down Expand Up @@ -228,6 +238,14 @@ bindEvent.reactiveExpr <- function(x, ..., ignoreNULL = TRUE, ignoreInit = FALSE

class(res) <- c("reactive.event", x_classes)

impl <- attr(res, "observable", exact = TRUE)
impl$.otelAttrs <- x_otel_attrs
if (!is.null(call_srcref)) {
otelAttrs <- otel_srcref_attributes(call_srcref)
# Overwrite any existing attributes with these new ones
# (such as code.filepath, code.lineno, code.column)
impl$.otelAttrs[names(otelAttrs)] <- otelAttrs
}
if (has_otel_bind("reactivity")) {
res <- bind_otel_reactive_expr(res)
}
Expand Down Expand Up @@ -260,6 +278,7 @@ bindEvent.shiny.render.function <- function(x, ..., ignoreNULL = TRUE, ignoreIni
)
}

# Passes over the otelAttrs from valueFunc to renderFunc
renderFunc <- addAttributes(renderFunc, renderFunctionAttributes(valueFunc))
class(renderFunc) <- c("shiny.render.function.event", class(valueFunc))
renderFunc
Expand All @@ -280,7 +299,17 @@ bindEvent.Observer <- function(x, ..., ignoreNULL = TRUE, ignoreInit = FALSE,

# Note that because the observer will already have been logged by this point,
# this updated label won't show up in the reactlog.
x$.label <- label %||% sprintf('bindEvent(%s, %s)', x$.label, quos_to_label(qs))
if (is.null(label)) {
call_srcref <- attr(sys.call(-1), "srcref", exact = TRUE)
x$.label <- rassignSrcrefToLabel(
call_srcref,
defaultLabel = as_default_label(
sprintf('bindEvent(%s, %s)', x$.label, quos_to_label(qs))
)
)
} else {
x$.label <- label
}

initialized <- FALSE

Expand Down Expand Up @@ -313,9 +342,17 @@ bindEvent.Observer <- function(x, ..., ignoreNULL = TRUE, ignoreInit = FALSE,
)

class(x) <- c("Observer.event", class(x))
call_srcref <- attr(sys.call(-1), "srcref", exact = TRUE)
if (!is.null(call_srcref)) {
otelAttrs <- otel_srcref_attributes(call_srcref)
# Overwrite any existing attributes with these new ones
# (such as code.filepath, code.lineno, code.column)
x$.otelAttrs[names(otelAttrs)] <- otelAttrs
}
if (has_otel_bind("reactivity")) {
x <- bind_otel_observe(x)
}

invisible(x)
}

Expand Down
20 changes: 13 additions & 7 deletions R/extended-task.R
Original file line number Diff line number Diff line change
Expand Up @@ -119,9 +119,9 @@ ExtendedTask <- R6Class("ExtendedTask", portable = TRUE, cloneable = FALSE,

# Do not show these private reactive values in otel spans
with_no_otel_bind({
private$rv_status <- reactiveVal("initial")
private$rv_value <- reactiveVal(NULL)
private$rv_error <- reactiveVal(NULL)
private$rv_status <- reactiveVal("initial", label = "ExtendedTask$private$status")
private$rv_value <- reactiveVal(NULL, label = "ExtendedTask$private$value")
private$rv_error <- reactiveVal(NULL, label = "ExtendedTask$private$error")
})

private$invocation_queue <- fastmap::fastqueue()
Expand All @@ -134,12 +134,16 @@ ExtendedTask <- R6Class("ExtendedTask", portable = TRUE, cloneable = FALSE,
call_srcref <- attr(sys.call(-1), "srcref", exact = TRUE)
label <- rassignSrcrefToLabel(
call_srcref,
defaultLabel = "<anonymous>",
fnName = "ExtendedTask\\$new"
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_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(
Expand Down Expand Up @@ -174,7 +178,7 @@ ExtendedTask <- R6Class("ExtendedTask", portable = TRUE, cloneable = FALSE,
private$otel_label_add_to_queue,
severity = "debug",
attributes = c(
otel_session_id_attrs(getDefaultReactiveDomain()),
private$otel_attrs,
list(
queue_size = private$invocation_queue$size() + 1L
)
Expand All @@ -186,7 +190,7 @@ ExtendedTask <- R6Class("ExtendedTask", portable = TRUE, cloneable = FALSE,
if (has_otel_bind("reactivity")) {
private$ospan <- create_shiny_ospan(
private$otel_label,
attributes = otel_session_id_attrs(getDefaultReactiveDomain())
attributes = private$otel_attrs
)
otel::local_active_span(private$ospan)
}
Expand Down Expand Up @@ -257,7 +261,9 @@ ExtendedTask <- R6Class("ExtendedTask", portable = TRUE, cloneable = FALSE,
rv_value = NULL,
rv_error = NULL,
invocation_queue = NULL,

otel_label = NULL,
otel_attrs = list(),
otel_label_add_to_queue = NULL,
ospan = NULL,

Expand Down
54 changes: 28 additions & 26 deletions R/mock-session.R
Original file line number Diff line number Diff line change
Expand Up @@ -436,34 +436,36 @@ MockShinySession <- R6Class(
if (!is.function(func))
stop(paste("Unexpected", class(func), "output for", name))

obs <- observe({
# We could just stash the promise, but we get an "unhandled promise error". This bypasses
prom <- NULL
tryCatch({
v <- private$withCurrentOutput(name, func(self, name))
if (!promises::is.promise(v)){
# Make our sync value into a promise
prom <- promises::promise(function(resolve, reject){ resolve(v) })
} else {
prom <- v
}
}, error=function(e){
# Error running value()
prom <<- promises::promise(function(resolve, reject){ reject(e) })
})

private$outs[[name]]$promise <- hybrid_chain(
prom,
function(v){
list(val = v, err = NULL)
}, catch=function(e){
if (
!inherits(e, c("shiny.custom.error", "shiny.output.cancel", "shiny.output.progress", "shiny.silent.error"))
) {
self$unhandledError(e, close = FALSE)
with_no_otel_bind({
obs <- observe({
# We could just stash the promise, but we get an "unhandled promise error". This bypasses
prom <- NULL
tryCatch({
v <- private$withCurrentOutput(name, func(self, name))
if (!promises::is.promise(v)){
# Make our sync value into a promise
prom <- promises::promise(function(resolve, reject){ resolve(v) })
} else {
prom <- v
}
list(val = NULL, err = e)
}, error=function(e){
# Error running value()
prom <<- promises::promise(function(resolve, reject){ reject(e) })
})

private$outs[[name]]$promise <- hybrid_chain(
prom,
function(v){
list(val = v, err = NULL)
}, catch=function(e){
if (
!inherits(e, c("shiny.custom.error", "shiny.output.cancel", "shiny.output.progress", "shiny.silent.error"))
) {
self$unhandledError(e, close = FALSE)
}
list(val = NULL, err = e)
})
})
})
private$outs[[name]] <- list(obs = obs, func = func, promise = NULL)
},
Expand Down
Loading
Loading