Skip to contents

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)