Nothing
# ============================================================================
# OPENTELEMETRY (OTEL) TEST HELPERS
# ============================================================================
# these helpers generate realistic OTLP-formatted test data for testing
# bidux's otel integration with shiny traces exported via shiny 1.12+
#' skip tests if otel integration not available
#' @keywords internal
skip_if_no_otel <- function() {
# check if otel-specific functions are available in bidux
# this will be updated once agent 1 implements otel functions
if (!exists("read_otel_json", where = asNamespace("bidux"), mode = "function")) {
skip("otel integration not yet implemented")
}
}
#' create mock otel spans dataframe with realistic shiny telemetry
#'
#' @description
#' generates a tibble of otel spans that mimic what shiny 1.12+ exports
#' when instrumented with opentelemetry. includes all common span types:
#' session_start, session_end, reactive, observe, output, and error events.
#'
#' @param sessions number of unique sessions to generate
#' @param reactives_per_session average number of reactive executions per session
#' @param outputs_per_session average number of output renders per session
#' @param include_errors whether to include error span events
#' @param slow_rate proportion of operations that should be slow (for perf testing)
#' @param seed random seed for reproducibility
#'
#' @return tibble with columns: trace_id, span_id, parent_span_id, name,
#' start_time_unix_nano, end_time_unix_nano, attributes (list column),
#' events (list column)
#'
#' @keywords internal
create_mock_otel_spans <- function(sessions = 3,
reactives_per_session = 10,
outputs_per_session = 5,
include_errors = TRUE,
slow_rate = 0.2,
seed = 123) {
set.seed(seed)
spans <- list()
for (session_num in seq_len(sessions)) {
session_id <- sprintf("session_%03d", session_num)
# generate random 32-character hex string for trace id
hex_chars <- c('0','1','2','3','4','5','6','7','8','9','a','b','c','d','e','f')
trace_id <- paste0(sample(hex_chars, 32, replace = TRUE), collapse = '')
# session start time (unix nanoseconds)
base_time <- as.numeric(Sys.time()) - (sessions - session_num) * 3600
session_start_nano <- as.character(floor(base_time * 1e9))
# session_start span
# generate random 16-character hex string for span id
session_span_id <- paste0(sample(hex_chars, 16, replace = TRUE), collapse = '')
spans[[length(spans) + 1]] <- list(
traceId = trace_id,
spanId = session_span_id,
parentSpanId = NA_character_,
name = "session_start",
startTimeUnixNano = session_start_nano,
endTimeUnixNano = as.character(floor(base_time * 1e9) + 1e6), # 1ms later
attributes = list(
list(key = "session.id", value = list(stringValue = session_id)),
list(key = "http.method", value = list(stringValue = "GET")),
list(key = "http.target", value = list(stringValue = "/"))
),
events = list()
)
current_time_nano <- as.numeric(session_start_nano) + 5e9 # 5 seconds after login
# reactive spans
num_reactives <- max(1, round(rnorm(1, reactives_per_session, 3)))
for (reactive_num in seq_len(num_reactives)) {
reactive_id <- sample(c("data_filter", "selected_region", "date_range", "metric_choice"), 1)
duration_ms <- if (runif(1) < slow_rate) rnorm(1, 500, 100) else rnorm(1, 50, 10)
duration_ns <- as.integer(max(1e6, duration_ms * 1e6))
# generate random 16-character hex string for span id
reactive_span_id <- paste0(sample(hex_chars, 16, replace = TRUE), collapse = '')
spans[[length(spans) + 1]] <- list(
traceId = trace_id,
spanId = reactive_span_id,
parentSpanId = session_span_id,
name = "reactive",
startTimeUnixNano = as.character(floor(current_time_nano)),
endTimeUnixNano = as.character(floor(current_time_nano + duration_ns)),
attributes = list(
list(key = "session.id", value = list(stringValue = session_id)),
list(key = "reactive.label", value = list(stringValue = reactive_id)),
list(key = "input_id", value = list(stringValue = reactive_id))
),
events = list()
)
current_time_nano <- current_time_nano + duration_ns + runif(1, 1e8, 5e8)
}
# output spans
num_outputs <- max(1, round(rnorm(1, outputs_per_session, 2)))
for (output_num in seq_len(num_outputs)) {
output_id <- sample(c("plot1", "table1", "summary_text", "map_view"), 1)
duration_ms <- if (runif(1) < slow_rate) rnorm(1, 1000, 200) else rnorm(1, 200, 50)
duration_ns <- as.integer(max(1e6, duration_ms * 1e6))
# generate random 16-character hex string for span id
output_span_id <- paste0(sample(hex_chars, 16, replace = TRUE), collapse = '')
output_attrs <- list(
list(key = "session.id", value = list(stringValue = session_id)),
list(key = "output.name", value = list(stringValue = output_id)),
list(key = "output_id", value = list(stringValue = output_id))
)
# add error event to some outputs
output_events <- list()
if (include_errors && runif(1) < 0.1) { # 10% error rate
error_msg <- sample(c(
"object 'data' not found",
"subscript out of bounds",
"cannot open connection",
"non-numeric argument to binary operator"
), 1)
output_events <- list(
list(
name = "exception",
time_unix_nano = as.character(floor(current_time_nano + duration_ns * 0.5)),
attributes = list(
list(key = "exception.type", value = list(stringValue = "error")),
list(key = "exception.message", value = list(stringValue = error_msg)),
list(key = "error.message", value = list(stringValue = error_msg))
)
)
)
}
spans[[length(spans) + 1]] <- list(
traceId = trace_id,
spanId = output_span_id,
parentSpanId = session_span_id,
name = paste0("output:", output_id),
startTimeUnixNano = as.character(floor(current_time_nano)),
endTimeUnixNano = as.character(floor(current_time_nano + duration_ns)),
attributes = output_attrs,
events = output_events
)
current_time_nano <- current_time_nano + duration_ns + runif(1, 5e8, 2e9)
}
# occasional navigation spans
if (runif(1) < 0.3) {
nav_page <- sample(c("dashboard", "analysis", "settings", "help"), 1)
# generate random 16-character hex string for span id
nav_span_id <- paste0(sample(hex_chars, 16, replace = TRUE), collapse = '')
spans[[length(spans) + 1]] <- list(
traceId = trace_id,
spanId = nav_span_id,
parentSpanId = session_span_id,
name = "navigation",
startTimeUnixNano = as.character(floor(current_time_nano)),
endTimeUnixNano = as.character(floor(current_time_nano + 1e6)),
attributes = list(
list(key = "session.id", value = list(stringValue = session_id)),
list(key = "navigation.target", value = list(stringValue = nav_page)),
list(key = "navigation_id", value = list(stringValue = nav_page))
),
events = list()
)
current_time_nano <- current_time_nano + 1e6 + runif(1, 1e8, 5e8)
}
# session_end span
# generate random 16-character hex string for span id
end_span_id <- paste0(sample(hex_chars, 16, replace = TRUE), collapse = '')
spans[[length(spans) + 1]] <- list(
traceId = trace_id,
spanId = end_span_id,
parentSpanId = session_span_id,
name = "session_end",
startTimeUnixNano = as.character(floor(current_time_nano)),
endTimeUnixNano = as.character(floor(current_time_nano + 1e6)),
attributes = list(
list(key = "session.id", value = list(stringValue = session_id))
),
events = list()
)
}
# convert to tibble
tibble::tibble(
traceId = vapply(spans, function(s) s$traceId, character(1)),
spanId = vapply(spans, function(s) s$spanId, character(1)),
parentSpanId = vapply(spans, function(s) s$parentSpanId %||% NA_character_, character(1)),
name = vapply(spans, function(s) s$name, character(1)),
startTimeUnixNano = vapply(spans, function(s) s$startTimeUnixNano, character(1)),
endTimeUnixNano = vapply(spans, function(s) s$endTimeUnixNano, character(1)),
attributes = lapply(spans, function(s) s$attributes),
events = lapply(spans, function(s) s$events)
)
}
#' create temporary otlp json file from spans dataframe
#'
#' @description
#' exports spans to proper otlp json format and writes to temp file.
#' follows the opentelemetry protocol specification with resourceSpans,
#' scopeSpans, and spans hierarchy.
#'
#' @param spans_df tibble from create_mock_otel_spans()
#' @param file_path optional file path (defaults to tempfile)
#'
#' @return character file path to the created json file
#'
#' @keywords internal
create_temp_otel_json <- function(spans_df, file_path = NULL) {
if (is.null(file_path)) {
file_path <- tempfile(fileext = ".json")
}
# convert spans_df to otlp json structure
spans_list <- lapply(seq_len(nrow(spans_df)), function(i) {
span <- spans_df[i, ]
# get attributes list (no longer double-nested)
attrs <- span$attributes[[1]]
# get events list (no longer double-nested)
evts <- span$events[[1]]
if (is.null(evts) || length(evts) == 0) {
evts <- list()
}
list(
traceId = span$traceId,
spanId = span$spanId,
parentSpanId = if (is.na(span$parentSpanId)) NULL else span$parentSpanId,
name = span$name,
startTimeUnixNano = span$startTimeUnixNano,
endTimeUnixNano = span$endTimeUnixNano,
attributes = attrs,
events = evts
)
})
# wrap in otlp structure
otlp_structure <- list(
resourceSpans = list(
list(
resource = list(
attributes = list(
list(key = "service.name", value = list(stringValue = "shiny-app")),
list(key = "telemetry.sdk.language", value = list(stringValue = "r"))
)
),
scopeSpans = list(
list(
scope = list(
name = "shiny",
version = "1.12.0"
),
spans = spans_list
)
)
)
)
)
# write to file
jsonlite::write_json(
otlp_structure,
file_path,
auto_unbox = TRUE,
pretty = TRUE,
null = "null"
)
return(file_path)
}
#' create temporary otel sqlite database from spans dataframe
#'
#' @description
#' creates sqlite db with proper otel schema: spans, span_events, span_attributes tables.
#' this mimics what otel collectors export to sqlite backends.
#'
#' @param spans_df tibble from create_mock_otel_spans()
#' @param db_path optional db path (defaults to tempfile)
#'
#' @return character file path to the created sqlite database
#'
#' @keywords internal
create_temp_otel_sqlite <- function(spans_df, db_path = NULL) {
if (!requireNamespace("DBI", quietly = TRUE) ||
!requireNamespace("RSQLite", quietly = TRUE)) {
skip("dbi and rsqlite required for otel sqlite tests")
}
if (is.null(db_path)) {
db_path <- tempfile(fileext = ".sqlite")
}
con <- DBI::dbConnect(RSQLite::SQLite(), db_path)
tryCatch({
# create spans table
spans_table <- data.frame(
traceId = spans_df$traceId,
spanId = spans_df$spanId,
parentSpanId = spans_df$parentSpanId,
name = spans_df$name,
startTimeUnixNano = spans_df$startTimeUnixNano,
endTimeUnixNano = spans_df$endTimeUnixNano,
stringsAsFactors = FALSE
)
DBI::dbWriteTable(con, "spans", spans_table, overwrite = TRUE)
# create span_attributes table (flattened)
attrs_rows <- list()
for (i in seq_len(nrow(spans_df))) {
span_id <- spans_df$spanId[i]
# attributes is a list of attribute objects (no longer double-nested)
attrs <- spans_df$attributes[[i]]
if (!is.null(attrs) && length(attrs) > 0) {
for (attr in attrs) {
attrs_rows[[length(attrs_rows) + 1]] <- list(
span_id = span_id,
key = attr$key,
value = attr$value$stringValue %||%
attr$value$intValue %||%
attr$value$doubleValue %||%
attr$value$boolValue %||%
NA_character_
)
}
}
}
if (length(attrs_rows) > 0) {
attrs_table <- data.frame(
span_id = vapply(attrs_rows, function(r) r$span_id, character(1)),
key = vapply(attrs_rows, function(r) r$key, character(1)),
value = vapply(attrs_rows, function(r) as.character(r$value), character(1)),
stringsAsFactors = FALSE
)
DBI::dbWriteTable(con, "span_attributes", attrs_table, overwrite = TRUE)
}
# create span_events table (flattened)
events_rows <- list()
for (i in seq_len(nrow(spans_df))) {
span_id <- spans_df$spanId[i]
# events is a list of event objects (no longer double-nested)
events <- spans_df$events[[i]]
if (!is.null(events) && length(events) > 0) {
for (event in events) {
event_attrs <- event$attributes
error_msg <- NA_character_
if (!is.null(event_attrs) && length(event_attrs) > 0) {
for (attr in event_attrs) {
if (attr$key %in% c("error.message", "exception.message")) {
error_msg <- attr$value$stringValue
}
}
}
events_rows[[length(events_rows) + 1]] <- list(
span_id = span_id,
name = event$name,
time_unix_nano = event$time_unix_nano,
error_message = error_msg
)
}
}
}
if (length(events_rows) > 0) {
events_table <- data.frame(
span_id = vapply(events_rows, function(r) r$span_id, character(1)),
name = vapply(events_rows, function(r) r$name, character(1)),
time_unix_nano = vapply(events_rows, function(r) r$time_unix_nano, character(1)),
error_message = vapply(events_rows, function(r) r$error_message %||% NA_character_, character(1)),
stringsAsFactors = FALSE
)
DBI::dbWriteTable(con, "span_events", events_table, overwrite = TRUE)
}
DBI::dbDisconnect(con)
}, error = function(e) {
if (!is.null(con) && DBI::dbIsValid(con)) {
DBI::dbDisconnect(con)
}
stop(e)
})
return(db_path)
}
#' create shiny.telemetry format json for comparison tests
#'
#' @description
#' creates traditional shiny.telemetry json format for testing
#' backward compatibility and format detection.
#'
#' @param sessions number of sessions
#' @param file_path optional file path
#'
#' @return character file path to created json file
#'
#' @keywords internal
create_temp_shiny_telemetry_json <- function(sessions = 2, file_path = NULL) {
if (is.null(file_path)) {
file_path <- tempfile(fileext = ".json")
}
events <- create_test_telemetry_events(sessions)
# write as json lines
lines <- vapply(events, function(e) {
jsonlite::toJSON(e, auto_unbox = TRUE)
}, character(1))
writeLines(lines, file_path)
return(file_path)
}
#' extract attribute value from otel attributes list
#'
#' @description
#' helper to extract specific attribute value from otel attributes structure.
#' handles the nested value.stringValue / value.intValue structure.
#'
#' @param attributes list of otel attributes
#' @param key attribute key to find
#' @param default default value if not found
#'
#' @return extracted value or default
#'
#' @keywords internal
extract_otel_attribute <- function(attributes, key, default = NA_character_) {
if (is.null(attributes) || length(attributes) == 0) {
return(default)
}
for (attr in attributes) {
if (!is.null(attr$key) && attr$key == key) {
# try different value types
value <- attr$value$stringValue %||%
attr$value$intValue %||%
attr$value$doubleValue %||%
attr$value$boolValue
return(if (is.null(value)) default else value)
}
}
return(default)
}
Any scripts or data that you put into this service are public.
Add the following code to your website.
For more information on customizing the embed code, read Embedding Snippets.