diff --git a/DESCRIPTION b/DESCRIPTION index e15c50d..5c5e12c 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -1,7 +1,7 @@ Package: shinyloadtest Type: Package Title: Load Test Shiny Applications -Version: 1.2.0 +Version: 1.2.0.9000 Authors@R: c( person("Barret", "Schloerke", role = c("aut", "cre"), email = "barret@posit.co", comment = c(ORCID = "0000-0001-9986-114X")), person("Alan", "Dipert", role = c("aut")), @@ -59,12 +59,12 @@ Suggests: rmarkdown, testthat (>= 3.2.0), spelling -RoxygenNote: 7.3.1 +RoxygenNote: 7.3.2 Roxygen: list(markdown = TRUE) SystemRequirements: pandoc (>= 2.2) - http://pandoc.org Language: en-US -Config/Needs/website: devtools, readr, gh, tidyverse/tidytemplate +Config/Needs/website: devtools, httr, readr, gh, tidyverse/tidytemplate Config/Needs/routine: devtools, readr, diff --git a/NEWS.md b/NEWS.md index bfcd655..c47fb87 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,3 +1,9 @@ +# shinyloadtest (development version) + +### Bug Fixes + +* Fixed #179: Waterfall plot labels now support all message types as of {shiny} v1.9.1. This will remove the empty `Set: ` and `Updated: ` labels from the waterfall plot and replace them with an appropriate label. + # shinyloadtest 1.2.0 ### Bug Fixes @@ -6,8 +12,6 @@ * Fixed #163: `gtable_trim` error during `shinyloadtest_report` with newer versions of ggplot2. - - # shinyloadtest 1.1.0 * `record_session()` gained a new variable `connect_api_key` to be able to diff --git a/R/analysis.R b/R/analysis.R index 8dd60be..e9c4a2d 100644 --- a/R/analysis.R +++ b/R/analysis.R @@ -7,6 +7,10 @@ strip_suffix <- function(str) { sub("(.*)_.*", "\\1", str) } +comma_collapse <- function(...) { + paste0(..., collapse = ", ") +} + # Read a "sessions/" directory full of .log files read_log_dir <- function(dir, name = basename(dirname(dir)), verbose = vroom::vroom_progress()) { @@ -137,7 +141,8 @@ recording_item_labels <- function(x_list) { for (i in seq_along(x_list)) { x <- x_list[[i]] - new_label <- switch(x$type, + new_label <- switch( + x$type, "REQ_HOME" = "Get: Homepage", "REQ_GET" = paste0("Get: ", shorten_url(x$url)), "REQ_TOK" = "Get: Shiny Token", @@ -161,7 +166,11 @@ recording_item_labels <- function(x_list) { } else { name_vals <- names(message$data) visible_name_vals <- name_vals[!grepl("^\\.", name_vals)] - paste0("Set: ", paste0(visible_name_vals, collapse = ", ")) + if (length(visible_name_vals) == 0) { + "(Empty update)" + } else { + paste0("Set: ", comma_collapse(visible_name_vals)) + } } } } @@ -170,12 +179,7 @@ recording_item_labels <- function(x_list) { if (x$message == "o") { "Start Connection" } else { - message <- x$message_parsed - if (!is.null(message$response$tag)) { - "Completed File Upload" - } else { - paste0("Updated: ", paste0(names(message$values), collapse = ", ")) - } + ws_recv_label(x$message_parsed) } }, "WS_CLOSE" = WS_CLOSE_LABEL, @@ -380,3 +384,184 @@ maintenance_df_ids <- function(df) { ) %>% unlist() } + + +ws_recv_label <- function(message) { + # When the day comes that multiple messages can be sent at once, we will need + # to rewright this function to handle that case. Until then, we can assume + # that only one message style is sent at a time and can return early. + + # Seach for usage of `$sendMessage(` in https://github.com/rstudio/shiny/blob/d84aa94762b4ffaf7533a007b6cb92c40f4f29af/R/shiny.R + + # Config handler is handled in `WS_RECV_INIT` + # `recalculating` is not to be shown + # `progress` is not to be shown + # `busy` is not to be shown + + # File upload complete + if (!is.null(message$response$tag)) { + return("Completed File Upload") + } + + # Update outputs, Input messages, Output errors (skipped) + if (!is.null(message$values)) { + # (choice): Do not display errors or input values + if (!is.null(message$errors)) { + paste0("Errors: ", comma_collapse(message$errors)) + } + + has_input_msgs <- + !is.null(message$inputMessages) && + length(message$inputMessages) > 0 + has_values <- length(message$values) > 0 + errors <- message$errors + has_errors <- length(errors) > 0 + if (has_errors) { + is_not_silent_error <- vapply(errors, function(err) { + ! ("shiny.silent.error" %in% err$type) + }, logical(1)) + errors <- errors[is_not_silent_error] + has_errors <- length(errors) > 0 + } + + if (!(has_errors || has_values || has_input_msgs)) { + return("(Empty values)") + } + + error_msgs <- NULL + if (has_errors) { + error_msgs <- paste0( + "Errors: ", comma_collapse(names(errors)) + ) + } + + input_msgs <- NULL + if (has_input_msgs) { + input_msgs <- paste0( + "Input message: ", comma_collapse(as.list(message$inputMessages)$id) + ) + } + + updated_msgs <- NULL + if (has_values) { + updated_msgs <- paste0( + "Updated: ", comma_collapse(names(message$values)) + ) + } + + return( + paste0(c(input_msgs, updated_msgs, error_msgs), collapse = "; ") + ) + } + + # Custom message handler + if (!is.null(message$custom)) { + custom_names <- + unlist(Map( + names(message$custom), + message$custom, + f = function(name, value) { + if (is.list(value) && !is.null(value$id)) { + paste0(name, "[", value$id, "]") + } else { + name + } + } + )) + return(paste0("Custom: ", comma_collapse(custom_names))) + } + + # Frozen message + if (!is.null(message$frozen)) { + return(paste0("Freeze: ", comma_collapse(message$frozen$ids))) + } + + # Unique Request/Response handler (`window.Shiny.shinyapp.makeRequest()` response) + if (!is.null(message$response)) { + # TODO future; Keep a map of request `tag` values to `method` values + # This would allow for the (typically discriptive) request method + # `Request: ` to be displayed, and not just `Request: ` + return(paste0("Request: ", message$response$tag)) + } + + # Notification handler + if (!is.null(message$notification)) { + return(switch( + message$notification$type, + "show" = paste0("Show notification: ", message$notification$message$id), + "remove" = paste0("Remove notification: ", message$notification$message), + "Notification: (Unknown)" + )) + } + + # Modal handler + if (!is.null(message$modal)) { + return(switch( + message$modal$type, + "show" = "Show modal", + "remove" = "Hide modal", + "Modal: (Unknown)" + )) + } + + # Reload handler + if (!is.null(message$reload)) { + return("Reload app") + } + + # # shiny-insert-ui handler + if (!is.null(message$`shiny-insert-ui`)) { + return(paste0("Insert UI: ", message$`shiny-insert-ui`$selector)) + } + + # # shiny-remove-ui handler + if (!is.null(message$`shiny-remove-ui`)) { + return(paste0("Remove UI: ", message$`shiny-remove-ui`$selector)) + } + + # # shiny-insert-tab handler + if (!is.null(message$`shiny-insert-tab`)) { + return(paste0("Insert tab: ", message$`shiny-insert-tab`$inputId)) + } + + # # shiny-remove-tab handler + if (!is.null(message$`shiny-remove-tab`)) { + return(paste0("Remove tab: ", message$`shiny-remove-tab`$inputId)) + } + + # # shiny-change-tab-visibility handler + if (!is.null(message$`shiny-change-tab-visibility`)) { + + tab_vis <- message$`shiny-change-tab-visibility` + return(switch( + tab_vis$type, + "show" = paste0("Show tab: ", tab_vis$inputId), + "hide" = paste0("Hide tab: ", tab_vis$inputId), + "Tab visibility: (Unknown)" + )) + } + + # # updateQueryString handler + if (!is.null(message$updateQueryString)) { + # Contents are too large. We should not display them + return("Update query string") + } + + # # resetBrush handler + if (!is.null(message$resetBrush)) { + return(paste0("Reset brush: ", message$resetBrush$brushId)) + } + + + msg_json <- to_json(message) + issue_url <- "https://github.com/rstudio/shinyloadtest/issues/new" + cli::cli_warn( + c( + "!" = "Unknown WS_RECV message", + "i" = "Please report this issue to {.url {issue_url} }", + "i" = paste0("{.code {msg_json} }") + ), + ) + + "(Unknown)" +} diff --git a/R/auth.R b/R/auth.R index 9873ab7..0b23c5f 100644 --- a/R/auth.R +++ b/R/auth.R @@ -72,12 +72,11 @@ postLogin <- function(appUrl, appServer, username, password) { RSC = handlePost( handle = curl::new_handle(), loginUrl = loginUrl, - postfields = jsonlite::toJSON( + postfields = to_json( list( username = username, password = password - ), - auto_unbox = TRUE + ) ), cookies = cookies, cookieName = "rsconnect" diff --git a/R/shiny-recorder.R b/R/shiny-recorder.R index 2131701..d792e24 100644 --- a/R/shiny-recorder.R +++ b/R/shiny-recorder.R @@ -157,12 +157,12 @@ makeWSEvent <- function(type, begin = Sys.time(), ...) { #' @export format.REQ <- function(x, ...) { - jsonlite::toJSON(unclass(x), auto_unbox = TRUE) + to_json(x) } #' @export format.WS <- function(x, ...) { - jsonlite::toJSON(unclass(x), auto_unbox = TRUE) + to_json(x) } shouldIgnore <- function(msg) { @@ -263,8 +263,10 @@ RecordingSession <- R6::R6Class("RecordingSession", clientWsState = CLIENT_WS_STATE$UNOPENED, postFiles = character(0), writeEvent = function(evt) { - writeLines(format(evt), private$outputFile) - flush(private$outputFile) + try({ + writeLines(format(evt), private$outputFile) + flush(private$outputFile) + }) }, initializeSessionCookies = function() { cookies <- data.frame() diff --git a/R/util.R b/R/util.R index cc0b765..1abf6f6 100644 --- a/R/util.R +++ b/R/util.R @@ -63,3 +63,11 @@ assert_is_available <- function(package, version = NULL) { )) } } + + +to_json <- function(x, ..., auto_unbox = TRUE, unclass_x = TRUE) { + if (unclass_x) { + x <- unclass(x) + } + jsonlite::toJSON(x, ..., auto_unbox = auto_unbox) +} diff --git a/data/slt_demo_data_1.rda b/data/slt_demo_data_1.rda index 625006f..9218702 100644 Binary files a/data/slt_demo_data_1.rda and b/data/slt_demo_data_1.rda differ diff --git a/data/slt_demo_data_16.rda b/data/slt_demo_data_16.rda index 9230962..4414f7d 100644 Binary files a/data/slt_demo_data_16.rda and b/data/slt_demo_data_16.rda differ diff --git a/data/slt_demo_data_4.rda b/data/slt_demo_data_4.rda index 875f9cd..088d0d4 100644 Binary files a/data/slt_demo_data_4.rda and b/data/slt_demo_data_4.rda differ diff --git a/tests/testthat/label-out/recording.log b/tests/testthat/label-out/recording.log new file mode 100644 index 0000000..03fa8ab --- /dev/null +++ b/tests/testthat/label-out/recording.log @@ -0,0 +1,72 @@ +# version: 1 +# target_url: http://127.0.0.1:5064 +# target_type: R/Shiny +{"type":"REQ_HOME","begin":"2024-08-21T16:20:29.838Z","end":"2024-08-21T16:20:29.903Z","status":200,"url":"/"} +{"type":"REQ_GET","begin":"2024-08-21T16:20:29.936Z","end":"2024-08-21T16:20:29.944Z","status":200,"url":"/shiny-javascript-1.9.1/shiny.min.js"} +{"type":"REQ_GET","begin":"2024-08-21T16:20:29.956Z","end":"2024-08-21T16:20:29.957Z","status":200,"url":"/bslib-component-css-0.8.0/components.css"} +{"type":"REQ_GET","begin":"2024-08-21T16:20:29.962Z","end":"2024-08-21T16:20:29.962Z","status":200,"url":"/shiny-sass-1.9.1/shiny-sass.css"} +{"type":"REQ_GET","begin":"2024-08-21T16:20:29.964Z","end":"2024-08-21T16:20:29.968Z","status":200,"url":"/bootstrap-5.3.1/bootstrap.min.css"} +{"type":"REQ_GET","begin":"2024-08-21T16:20:29.970Z","end":"2024-08-21T16:20:29.972Z","status":200,"url":"/bootstrap-5.3.1/bootstrap.bundle.min.js"} +{"type":"REQ_GET","begin":"2024-08-21T16:20:29.996Z","end":"2024-08-21T16:20:29.997Z","status":200,"url":"/bootstrap-5.3.1/font.css"} +{"type":"REQ_GET","begin":"2024-08-21T16:20:30.026Z","end":"2024-08-21T16:20:30.027Z","status":200,"url":"/bootstrap-5.3.1/fonts/memvYaGs126MiZpBA-UvWbX2vVnXBbObj2OVTS-muw.woff2"} +{"type":"REQ_GET","begin":"2024-08-21T16:20:30.030Z","end":"2024-08-21T16:20:30.030Z","status":200,"url":"/bootstrap-5.3.1/fonts/HI_SiYsKILxRpg3hIP6sJ7fM7PqlPevW.woff2"} +{"type":"WS_OPEN","begin":"2024-08-21T16:20:30.036Z","url":"/websocket/"} +{"type":"WS_RECV_INIT","begin":"2024-08-21T16:20:30.040Z","message":"{\"config\":{\"workerId\":\"\",\"sessionId\":\"${SESSION}\",\"user\":null}}"} +{"type":"WS_SEND","begin":"2024-08-21T16:20:30.043Z","message":"{\"method\":\"init\",\"data\":{\"file1:shiny.file\":null,\"tabs\":null,\"showhide_tabs\":\"A\",\"btn_error:shiny.action\":0,\"btn_shinyjsToggle:shiny.action\":0,\"btn_freezeReactiveValue:shiny.action\":0,\"btn_notification_show:shiny.action\":0,\"btn_modal_show:shiny.action\":0,\"btn_insert_ui:shiny.action\":0,\"btn_insert_tab:shiny.action\":0,\"btn_toggle_tab:shiny.action\":0,\"btn_update_query_string:shiny.action\":0,\"btn_reset_brush:shiny.action\":0,\"btn_reload:shiny.action\":0,\"freeze_bxs\":[\"A\"],\"txt\":\"Click toggle button to make me disappear\",\".clientdata_output_file_output_hidden\":false,\".clientdata_output_error_output_hidden\":false,\".clientdata_output_freeze_bxs_txt_hidden\":false,\".clientdata_pixelratio\":1,\".clientdata_url_protocol\":\"http:\",\".clientdata_url_hostname\":\"127.0.0.1\",\".clientdata_url_port\":\"8600\",\".clientdata_url_pathname\":\"/\",\".clientdata_url_search\":\"\",\".clientdata_url_hash_initial\":\"\",\".clientdata_url_hash\":\"\",\".clientdata_singletons\":\"add739c82ab207ed2c80be4b7e4b181525eb7a75\"}}"} +{"type":"WS_RECV","begin":"2024-08-21T16:20:30.063Z","message":"{\"errors\":{\"error_output\":{\"message\":\"0\",\"call\":\"renderText()\",\"type\":null}},\"values\":{\"freeze_bxs_txt\":\"Selected boxes: A\",\"file_output\":\"(No file uploaded)\"},\"inputMessages\":[]}"} +{"type":"WS_SEND","begin":"2024-08-21T16:20:35.470Z","message":"{\"method\":\"uploadInit\",\"args\":[[{\"name\":\"py-shiny.png\",\"size\":193371,\"type\":\"image/png\"}]],\"tag\":0}"} +{"type":"WS_RECV_BEGIN_UPLOAD","begin":"2024-08-21T16:20:35.478Z","message":"{\"response\":{\"tag\":0,\"value\":{\"jobId\":\"${UPLOAD_JOB_ID}\",\"uploadUrl\":\"session/${SESSION}/upload/${UPLOAD_JOB_ID}?w=\"}}}"} +{"type":"WS_SEND","begin":"2024-08-21T16:20:37.182Z","message":"{\"method\":\"update\",\"data\":{\"btn_error:shiny.action\":1}}"} +{"type":"WS_RECV","begin":"2024-08-21T16:20:37.206Z","message":"{\"errors\":{\"error_output\":{\"message\":\"1\",\"call\":\"renderText()\",\"type\":null}},\"values\":{},\"inputMessages\":[]}"} +{"type":"WS_SEND","begin":"2024-08-21T16:20:38.065Z","message":"{\"method\":\"update\",\"data\":{\"btn_shinyjsToggle:shiny.action\":1}}"} +{"type":"WS_RECV","begin":"2024-08-21T16:20:38.077Z","message":"{\"custom\":{\"shinyjs-toggle\":{\"id\":\"txt\",\"anim\":false,\"animType\":\"slide\",\"time\":0.5,\"selector\":null,\"condition\":null,\"asis\":false}}}"} +{"type":"WS_SEND","begin":"2024-08-21T16:20:38.634Z","message":"{\"method\":\"update\",\"data\":{\"btn_freezeReactiveValue:shiny.action\":1}}"} +{"type":"WS_RECV","begin":"2024-08-21T16:20:38.642Z","message":"{\"frozen\":{\"ids\":[\"cols\"]}}"} +{"type":"WS_RECV","begin":"2024-08-21T16:20:38.649Z","message":"{\"errors\":{},\"values\":{},\"inputMessages\":[{\"id\":\"freeze_bxs\",\"message\":{\"options\":\"
\\n
\\n