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.

Shiny integration

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:

  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.

# 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_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:

# 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)

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.