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.
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:
-
session$user— when Shiny Server Pro / Posit Connect authentication is configured (the value in regulated production deployments) -
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.
# 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.
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.
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:
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:
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:
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_linter9. 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:
# 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.
# 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.
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:
observeEvent(input$load_dataset, {
dataset <- rl_read(log, haven::read_sas, input$dataset_path)
})12. Complete minimal 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)