--- title: "Shiny integration" output: rmarkdown::html_vignette vignette: > %\VignetteIndexEntry{Shiny integration} %\VignetteEngine{knitr::rmarkdown} %\VignetteEncoding{UTF-8} --- ```{r setup, include = FALSE} knitr::opts_chunk$set(collapse = TRUE, comment = "#>", eval = FALSE) library(regulog) ``` `regulog` integrates with Shiny through two dedicated functions and works naturally with all the standard `log_*` functions inside server logic. | Function | Purpose | |---|---| | `regulog_shiny_init()` | Initialise a session inside `server()` — resolves the authenticated user from `session$user` and auto-logs `session_start` / `session_end` | | `regulog_observer()` | Wrap `observeEvent()` to log an action whenever a reactive input fires | All other `regulog` functions — `log_action()`, `log_change()`, `log_note()`, `log_signature()`, `rl_read()`, `with_log()`, `filter_log()`, `export_audit_trail()` — work identically inside Shiny server functions. ## 1. Basic pattern Call `regulog_shiny_init()` at the top of `server()`. Store the returned log object as a local variable and pass it to `log_*` calls inside observers. ```{r basic} library(shiny) library(regulog) server <- function(input, output, session) { log <- regulog_shiny_init( session = session, app = "clinical-review-tool", version = "1.2.0", path = "logs/review_audit.rlog" ) observeEvent(input$approve, { req(nzchar(input$justification)) log_action(log, action = "approved", object = input$dataset_id, reason = input$justification ) showNotification("Approval recorded.", type = "message") }) observeEvent(input$reject, { req(nzchar(input$rejection_reason)) log_action(log, action = "rejected", object = input$dataset_id, reason = input$rejection_reason ) showNotification("Rejection recorded.", type = "warning") }) } ``` ## 2. User resolution `session$user` is the authenticated identity set by Shiny Server Pro or Posit Connect. `regulog_shiny_init()` resolves the user in this order: 1. `session$user` — when Shiny Server Pro / Posit Connect authentication is configured (the value in regulated production deployments) 2. `Sys.info()[["user"]]` — OS-level user, as a fallback with a warning In regulated environments, always configure authentication so that `session$user` carries a real, non-repudiable identity. The `user` field appears on every log entry and is the basis for user attribution under 21 CFR Part 11 §11.50. ```{r user-check} # In a development context, confirm what user is being captured: server <- function(input, output, session) { log <- regulog_shiny_init( session = session, app = "review-tool", version = "1.0.0" ) output$debug_user <- renderText({ paste("Logging as:", log$user) }) } ``` ## 3. Session lifecycle events `regulog_shiny_init()` automatically adds two entries — no additional code needed: | Entry | Trigger | `action` | `reason` | |---|---|---|---| | `session_start` | `regulog_shiny_init()` call | `"session_start"` | `"Shiny session opened"` | | `session_end` | `session$onSessionEnded()` | `"session_end"` | `"Shiny session closed"` | These bracket all user-driven entries and give a complete picture of each session — who was logged in, when, and for how long. ## 4. Using regulog_observer() `regulog_observer()` reduces boilerplate when many UI events need auditing. The `object` and `reason` arguments accept both fixed strings and reactive expressions. ```{r observer} server <- function(input, output, session) { log <- regulog_shiny_init( session = session, app = "data-review", version = "2.0.0", path = "logs/review.rlog" ) # Static strings regulog_observer(log, session, eventExpr = input$lock_database, action = "database_locked", object = "study_database", reason = "Database lock confirmed by data manager" ) # Reactive strings — evaluated at the time the event fires regulog_observer(log, session, eventExpr = input$approve_record, action = "approved", object = reactive(paste0("record_", input$record_id)), reason = reactive(input$approval_reason) ) regulog_observer(log, session, eventExpr = input$flag_discrepancy, action = "flagged", object = reactive(paste0("record_", input$record_id)), reason = reactive(paste0("Discrepancy: ", input$discrepancy_detail)) ) } ``` ## 5. Notes and decisions from Shiny Use `log_note()` inside observers to document analytical decisions made through the UI — retention of outliers, query resolutions, deviations. ```{r notes} observeEvent(input$retain_outlier, { req(nzchar(input$retention_rationale)) log_note(log, paste0( "Outlier retained for subject ", input$subject_id, " at ", input$visit_label, ": ", input$outlier_value, ". ", input$retention_rationale )) showNotification( paste("Retention decision logged for subject", input$subject_id), type = "message" ) }) observeEvent(input$resolve_query, { req(nzchar(input$query_resolution)) log_note(log, paste0( "Query Q-", input$query_id, " resolved: ", input$query_resolution, " (resolved by: ", session$user, ")" )) }) ``` ## 6. Logging data changes from Shiny When users edit records through a Shiny app, `log_change()` documents the before/after values: ```{r changes} observeEvent(input$save_correction, { req(input$original_value, input$corrected_value, input$correction_reason) log_change(log, object = paste0(input$subject_id, "_", input$field_name), field = input$field_name, before = input$original_value, after = input$corrected_value, reason = paste0( input$correction_reason, " — corrected by ", session$user ) ) showNotification("Correction recorded in audit trail.", type = "message") }) ``` ## 7. Electronic signatures from Shiny Require a meaningful sign-off before a dataset is locked or an export is authorised: ```{r signature} observeEvent(input$sign_off, { req(nzchar(input$signoff_meaning)) log_signature(log, meaning = input$signoff_meaning) # Export signed audit trail alongside the lock export_path <- sprintf( "exports/audit_%s_%s.csv", gsub("[^a-zA-Z0-9]", "_", input$study_id), format(Sys.Date(), "%Y%m%d") ) export_audit_trail(log, format = "csv", signed = TRUE, path = export_path ) showModal(modalDialog( title = "Sign-off complete", paste("Signature recorded. Audit trail exported to:", export_path), easyClose = TRUE )) }) ``` ## 8. Download handler for the audit trail Let reviewers download a signed CSV of the current session's log: ```{r download} output$download_audit <- downloadHandler( filename = function() { sprintf("audit_%s.csv", format(Sys.Date(), "%Y%m%d")) }, content = function(file) { export_audit_trail(log, format = "csv", signed = TRUE, path = file) } ) # In UI: # downloadButton("download_audit", "Download audit trail (CSV)") # nolint: commented_code_linter ``` ## 9. Per-session vs shared log ### Per-session (default) Each browser session gets its own independent audit trail. This is the default — `regulog_shiny_init()` creates a new log for each `server()` call. Individual `.rlog` files can later be combined for a study-level view. ### Shared log across users For applications where one audit file covers all user activity: ```{r shared} # In global.R — outside server(), evaluated once at startup shared_log <- regulog_init( app = "multi-user-review", version = "1.0.0", user = "system", # will be overridden per action path = "logs/shared_audit.rlog" ) server <- function(input, output, session) { # Resolve per-request user current_user <- reactive({ u <- session$user if (is.null(u) || !nzchar(u)) Sys.info()[["user"]] else u }) observeEvent(input$approve, { req(nzchar(input$reason)) log_action(shared_log, action = "approved", object = input$record_id, reason = input$reason, user = current_user() # explicit per-action user ) }) } ``` ## 10. Querying and verifying from outside the app Session logs are plain `.rlog` files and can be verified or queried at any time without a running Shiny app — important for QC reviewers who may not have access to the application itself. ```{r offline} # Verify integrity of a session log result <- verify_log("logs/review_audit.rlog") result$intact # All signatures filter_log("logs/review_audit.rlog", type = "SIGNATURE") # All actions by a specific user filter_log("logs/review_audit.rlog", type = "ACTION", user = "jsmith" ) # Changes made within a date range filter_log("logs/review_audit.rlog", type = "CHANGE", from = "2026-06-01", to = "2026-06-30" ) # Export signed CSV from file path — no session required export_audit_trail("logs/review_audit.rlog", format = "csv", signed = TRUE, path = "exports/review_audit_signed.csv" ) ``` ## 11. Logging data reads from Shiny Inside a server function, use `with_log()` to log data reads scoped to the current session. Because `with_log()` resolves its local `read()` binding through lexical scope rather than shared package state, this is safe under concurrent use — each browser session's reads are logged only to that session's own `log`, even when multiple users are active in the same R process at once. ```{r shiny-reads} server <- function(input, output, session) { log <- regulog_shiny_init( session = session, app = "data-review", version = "1.0.0", path = sprintf("logs/review_%s.rlog", session$token) ) observeEvent(input$load_dataset, { with_log(log, { dataset <- read(haven::read_sas, input$dataset_path) # nolint: object_usage_linter }) showNotification("Dataset loaded and logged.", type = "message") }) } ``` For a single read outside a scoped block, `rl_read()` is equivalent: ```{r shiny-rl-read} observeEvent(input$load_dataset, { dataset <- rl_read(log, haven::read_sas, input$dataset_path) }) ``` ## 12. Complete minimal example ```{r complete-example} library(shiny) library(regulog) ui <- fluidPage( titlePanel("Clinical Data Review"), sidebarLayout( sidebarPanel( selectInput("dataset_id", "Dataset", c("ADSL", "ADAE", "ADLB")), textAreaInput("justification", "Justification", rows = 3), actionButton("approve", "Approve", class = "btn-success"), actionButton("reject", "Reject", class = "btn-danger"), hr(), textAreaInput("note_text", "Add note", rows = 3), actionButton("add_note", "Add note"), hr(), textAreaInput("signoff_meaning", "Signature meaning", rows = 3), actionButton("sign_off", "Sign off"), hr(), downloadButton("download_audit", "Download audit trail") ), mainPanel( tableOutput("log_table") ) ) ) server <- function(input, output, session) { log <- regulog_shiny_init( session = session, app = "data-review", version = "1.0.0", path = tempfile(fileext = ".rlog") ) observeEvent(input$approve, { req(nzchar(input$justification)) log_action(log, "approved", input$dataset_id, input$justification) showNotification("Approval recorded.", type = "message") }) observeEvent(input$reject, { req(nzchar(input$justification)) log_action(log, "rejected", input$dataset_id, input$justification) showNotification("Rejection recorded.", type = "warning") }) observeEvent(input$add_note, { req(nzchar(input$note_text)) log_note(log, input$note_text) showNotification("Note recorded.", type = "message") }) observeEvent(input$sign_off, { req(nzchar(input$signoff_meaning)) log_signature(log, input$signoff_meaning) showNotification("Signature recorded.", type = "message") }) output$download_audit <- downloadHandler( filename = function() sprintf("audit_%s.csv", Sys.Date()), content = function(file) { export_audit_trail(log, format = "csv", signed = TRUE, path = file) } ) output$log_table <- renderTable({ input$approve input$reject input$add_note input$sign_off df <- filter_log(log) if (nrow(df) == 0L) { return(NULL) } df[, c("entry_id", "type", "action", "object", "reason")] }) } shinyApp(ui, server) ```