The hardware and bandwidth for this mirror is donated by dogado GmbH, the Webhosting and Full Service-Cloud Provider. Check out our Wordpress Tutorial.
If you wish to report a bug, or if you are interested in having us mirror your free-software or open-source project, please feel free to contact us at mirror[@]dogado.de.
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.
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")
})
}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 warningIn 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.
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.
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))
)
}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, ")"
))
})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")
})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
))
})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_linterSession 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"
)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:
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)These binaries (installable software) and packages are in development.
They may not be fully stable and should be used with caution. We make no claims about them.
Health stats visible at Monitor.