diff --git a/DESCRIPTION b/DESCRIPTION index c9c635d40..55450e741 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -33,7 +33,6 @@ Imports: glue, htmltools, htmlwidgets, - logger, magrittr, PKNCA (>= 0.12.1), plotly (>= 4.11.0), diff --git a/NAMESPACE b/NAMESPACE index 29d1b2d67..c8fdc9788 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -140,7 +140,7 @@ importFrom(glue,glue) importFrom(grid,convertUnit) importFrom(htmltools,tags) importFrom(htmlwidgets,onRender) -importFrom(logger,log_info) + importFrom(magrittr,`%>%`) importFrom(plotly,add_lines) importFrom(plotly,add_trace) diff --git a/R/imports-shiny.R b/R/imports-shiny.R index 119a2bfa7..2f6e36b92 100644 --- a/R/imports-shiny.R +++ b/R/imports-shiny.R @@ -7,7 +7,6 @@ #' @importFrom bslib page_sidebar #' @importFrom htmltools tags #' @importFrom htmlwidgets onRender -#' @importFrom logger log_info #' @importFrom reactable reactable #' @importFrom reactable.extras reactable_extras_dependency #' @importFrom shiny runApp diff --git a/inst/shiny/app.R b/inst/shiny/app.R index 55e5c4c96..26a8bb642 100644 --- a/inst/shiny/app.R +++ b/inst/shiny/app.R @@ -3,7 +3,6 @@ require(aNCA) require(bslib) require(dplyr) require(htmlwidgets) -require(logger) require(formatters) require(magrittr) require(plotly) diff --git a/inst/shiny/functions/logging.R b/inst/shiny/functions/logging.R new file mode 100644 index 000000000..ce29dedbc --- /dev/null +++ b/inst/shiny/functions/logging.R @@ -0,0 +1,84 @@ +# Lightweight logging system for the aNCA Shiny app. +# +# Replaces the `logger` package with console-only output and in-memory +# log capture for ZIP export. Supports glue-style interpolation and +# paste-style multi-argument calls. +# +# Log levels: TRACE < DEBUG < INFO < SUCCESS < WARN < ERROR +# Default threshold: INFO (configurable via aNCA_LOG_LEVEL env var). +# +# The log captures application-level events only — not raw R console +# output. Warnings and errors from third-party packages appear only +# when explicitly caught by tryCatch blocks with log_warn/log_error. +# +# The in-memory buffer is exported as session_log.txt in the ZIP +# download. For a full reference of logged events, see: +# https://pharmaverse.github.io/aNCA/articles/session_log.html + +.log_env <- new.env(parent = emptyenv()) +.log_env$threshold <- "INFO" +.log_env$buffer <- character(0) + +.LOG_LEVELS <- c(TRACE = 1L, DEBUG = 2L, INFO = 3L, SUCCESS = 4L, WARN = 5L, ERROR = 6L) + +#' Initialise the logging system. +#' +#' Reads the threshold from the `aNCA_LOG_LEVEL` environment variable +#' (default `"INFO"`) and clears the in-memory log buffer. +setup_logger <- function() { + level <- toupper(Sys.getenv("aNCA_LOG_LEVEL", "INFO")) + if (!level %in% names(.LOG_LEVELS)) level <- "INFO" + .log_env$threshold <- level + .log_env$buffer <- character(0) +} + +#' Core logging function. +#' @param level Character: one of the log level names. +#' @param ... Message parts. If the first argument contains `{`, it is +#' evaluated as a glue string in the caller's environment. Otherwise +#' all arguments are pasted together. +#' @noRd +.log_msg <- function(level, ...) { + if (.LOG_LEVELS[[level]] < .LOG_LEVELS[[.log_env$threshold]]) return(invisible(NULL)) + + args <- list(...) + if (length(args) == 0L) { + msg <- "" + } else if (length(args) == 1L && grepl("\\{", args[[1]])) { + msg <- tryCatch( + glue::glue(args[[1]], .envir = parent.frame(2)), + error = function(e) paste0(args, collapse = "") + ) + } else { + msg <- paste0(args, collapse = "") + } + + timestamp <- format(Sys.time(), "%Y-%m-%d %H:%M:%S") + line <- paste0("[", timestamp, "] ", level, ": ", msg) + + .log_env$buffer <- c(.log_env$buffer, line) + message(line) + invisible(NULL) +} + +# Public log functions matching the logger API +log_trace <- function(...) .log_msg("TRACE", ...) +log_debug <- function(...) .log_msg("DEBUG", ...) +log_info <- function(...) .log_msg("INFO", ...) +log_success <- function(...) .log_msg("SUCCESS", ...) +log_warn <- function(...) .log_msg("WARN", ...) +log_error <- function(...) .log_msg("ERROR", ...) + +#' Logs a list or data frame at DEBUG level. +#' +#' @param title Title for the log entry. +#' @param l List or data.frame to log. +log_debug_list <- function(title, l) { + log_debug(aNCA:::.concatenate_list(title, l)) +} + +#' Return the in-memory log buffer as a character vector. +#' @noRd +get_log_buffer <- function() { + .log_env$buffer +} diff --git a/inst/shiny/functions/utils-exclusions.R b/inst/shiny/functions/utils-exclusions.R index e16946cff..1372b358f 100644 --- a/inst/shiny/functions/utils-exclusions.R +++ b/inst/shiny/functions/utils-exclusions.R @@ -91,6 +91,12 @@ EXCL_COLOR_PARAM <- "#FFF3CD" # yellow — parameter exclusion exclusion_list(append(current, list_new_reason)) updateTextInput(session, "exclusion_reason", value = "") updateReactable(table_id, selected = NA) + + type_label <- .exclusion_type_label(nca_checked, tlg_checked) + log_info( + "Exclusion added: ", length(rows_sel), " rows, type=", type_label, + ", reason='", reason, "'" + ) } } @@ -211,7 +217,8 @@ EXCL_COLOR_PARAM <- "#FFF3CD" # yellow — parameter exclusion #' @param registered_ids reactiveVal holding character vector of registered IDs. #' @param input Shiny input object. #' @noRd -.register_remove_observers <- function(exclusion_list, registered_ids, input) { +.register_remove_observers <- function(exclusion_list, registered_ids, input, + on_remove = NULL) { lst <- exclusion_list() already <- registered_ids() new_ids <- setdiff( @@ -223,6 +230,10 @@ EXCL_COLOR_PARAM <- "#FFF3CD" # yellow — parameter exclusion local_id <- xbtn_id observeEvent(input[[local_id]], { current <- exclusion_list() + removed <- Filter(function(x) x$xbtn_id == local_id, current) + if (is.function(on_remove) && length(removed) > 0) { + on_remove(removed[[1]]) + } exclusion_list(Filter(function(x) x$xbtn_id != local_id, current)) }, ignoreInit = TRUE, once = TRUE) }) diff --git a/inst/shiny/functions/utils.R b/inst/shiny/functions/utils.R index ae247ed42..ba264a160 100644 --- a/inst/shiny/functions/utils.R +++ b/inst/shiny/functions/utils.R @@ -1,45 +1,3 @@ -#' Sets up the logger package for the application. -#' -#' @details -#' The application logs everything to a log file located in `/log` directory. If such folder -#' does not exist, it will be created. Logfile for each session will be separate. The application -#' will keep 5 log files at any given time - if this number is exceeded, the oldest log file -#' will be deleted. -#' -#' In addition, information of the level specified by the user will be logged to console. -#' As a default, this level is INFO - this is so that the user has good information on what is -#' happening inside the app, but is not overwhelmed with tracing and debugging information. This -#' level can be changed using `aNCA_LOG_LEVEL` environmental variable, set for example in -#' `.Renviron` file. -setup_logger <- function() { - log_layout(layout_glue_colors) - log_formatter(formatter_glue) - log_threshold(TRACE) - log_threshold(Sys.getenv("aNCA_LOG_LEVEL", "INFO"), index = 2) - - log_dir <- "./log" - if (!dir.exists(log_dir)) dir.create(log_dir) - existing_logs <- list.files(log_dir, full.names = TRUE) - if (length(existing_logs) >= 5) file.remove(sort(existing_logs)[1]) # keep only five log files - logfile_name <- paste0(log_dir, "/aNCA_app_", format(Sys.time(), "%y%m%d-%H%M%S-"), ".log") - - log_appender(appender_file(logfile_name)) - log_appender(appender_console, index = 2) -} - -#' Logs a list and data frame objects. -#' -#' @details -#' Utilitary function for logging a list object (like mapping list or used settings) in -#' a nice format. Parses a list into nice string and logs at DEBUG level. Can also process -#' data frames, which will be converted into a list of rows. -#' -#' @param title Title for the logs. -#' @param l List object to be parsed into log. Can also be a data.frame. -log_debug_list <- function(title, l) { - log_debug(aNCA:::.concatenate_list(title, l)) -} - #' Needed to properly reset reactable.extras widgets #' #' @details diff --git a/inst/shiny/functions/zip-utils.R b/inst/shiny/functions/zip-utils.R index b4101c35e..1cdb1b23f 100644 --- a/inst/shiny/functions/zip-utils.R +++ b/inst/shiny/functions/zip-utils.R @@ -454,6 +454,12 @@ prepare_export_files <- function(target_dir, detail = "Saving session info...") .export_session_info(target_dir) } + + if ("session_log" %in% input$res_tree) { + progress$set(message = "Creating exports...", + detail = "Saving session log...") + .export_session_log(target_dir) + } progress$inc(0.8) .clean_export_dir(target_dir, input, custom_names) @@ -685,6 +691,45 @@ prepare_export_files <- function(target_dir, writeLines(lines, file.path(target_dir, "session_info.txt")) } +#' Export the in-memory session log to a text file. +#' @param target_dir Target directory for the export. +#' @keywords internal +.export_session_log <- function(target_dir) { + log_buffer <- get_log_buffer() + + threshold <- .log_env$threshold + header <- c( + "# aNCA Session Log", + "#", + "# This file contains application events captured during your session.", + "# It records data upload, mapping, NCA settings, parameter selection,", + "# slope adjustments, exclusions, calculation results, and exports.", + "#", + "# Warnings and errors from these operations are included when caught", + "# by the application. Unexpected R errors or warnings from third-party", + "# packages that are not explicitly handled will NOT appear here.", + "#", + paste0("# Log level: ", threshold, + " (configurable via aNCA_LOG_LEVEL env var)"), + paste0("# Levels shown at current threshold: ", + paste(names(.LOG_LEVELS)[.LOG_LEVELS >= .LOG_LEVELS[[threshold]]], + collapse = ", ")), + "#", + "# For a full reference of logged events, see:", + "# https://pharmaverse.github.io/aNCA/articles/session_log.html", + "#", + paste0("# Generated: ", format(Sys.time(), "%Y-%m-%d %H:%M:%S %Z")), + "# -------------------------------------------------------------------", + "" + ) + + if (length(log_buffer) == 0L) { + log_buffer <- "(No log entries captured during this session.)" + } + + writeLines(c(header, log_buffer), file.path(target_dir, "session_log.txt")) +} + #' Clean Export Directory #' @param target_dir Target directory to clean #' @param input Shiny input object @@ -722,6 +767,10 @@ prepare_export_files <- function(target_dir, files_req <- c(files_req, grep("session_info\\.txt$", all_files, value = TRUE)) } + if ("session_log" %in% fnames) { + files_req <- c(files_req, grep("session_log\\.txt$", all_files, + value = TRUE)) + } file.remove(all_files[!all_files %in% files_req]) # Recursive directory cleanup — remove dirs that contain no files at any depth diff --git a/inst/shiny/modules/tab_nca/setup/data_imputation.R b/inst/shiny/modules/tab_nca/setup/data_imputation.R index eb5c5f6f9..a793c08ba 100644 --- a/inst/shiny/modules/tab_nca/setup/data_imputation.R +++ b/inst/shiny/modules/tab_nca/setup/data_imputation.R @@ -196,6 +196,15 @@ data_imputation_server <- function(id, settings_override) { } }) + observeEvent(input$select_blq_strategy, { + log_info("BLQ imputation strategy changed: ", input$select_blq_strategy) + }, ignoreInit = TRUE) + + observeEvent(input$should_impute_c0, { + state <- if (isTRUE(input$should_impute_c0)) "enabled" else "disabled" + log_info("Start concentration imputation ", state) + }, ignoreInit = TRUE) + blq_imputation_rule <- reactive({ req(input$select_blq_strategy) rule_list <- switch(input$select_blq_strategy, diff --git a/inst/shiny/modules/tab_nca/setup/general_exclusions.R b/inst/shiny/modules/tab_nca/setup/general_exclusions.R index 9a2fe9ce2..db118a3ed 100644 --- a/inst/shiny/modules/tab_nca/setup/general_exclusions.R +++ b/inst/shiny/modules/tab_nca/setup/general_exclusions.R @@ -131,6 +131,7 @@ general_exclusions_server <- function( xbtn_counter(max(new_ids)) exclusion_list(rehydrated_list) + log_info("Exclusions restored from settings: ", length(overrides), " rules loaded") } }) @@ -166,7 +167,12 @@ general_exclusions_server <- function( # Register observers for new remove buttons (shared helper) observe({ - .register_remove_observers(exclusion_list, registered_xbtns, input) + .register_remove_observers( + exclusion_list, registered_xbtns, input, + on_remove = function(item) { + log_info("Exclusion removed: reason='", item$reason, "'") + } + ) }) # Render the exclusions table (not shown if empty) diff --git a/inst/shiny/modules/tab_nca/setup/manual_slopes_table.R b/inst/shiny/modules/tab_nca/setup/manual_slopes_table.R index 163215844..967f1945d 100644 --- a/inst/shiny/modules/tab_nca/setup/manual_slopes_table.R +++ b/inst/shiny/modules/tab_nca/setup/manual_slopes_table.R @@ -84,7 +84,7 @@ manual_slopes_table_server <- function( # Add a new row to the table when the user clicks the add button observeEvent(input$add_rule, { - log_trace("{id}: adding manual slopes row") + log_trace("Slope selector: adding manual slope rule") first_group <- slopes_pknca_groups()[1, ] time_col <- pknca_data()$conc$columns$time new_row <- cbind( @@ -121,7 +121,7 @@ manual_slopes_table_server <- function( # Remove selected rows from the table when the user clicks the remove button observeEvent(input$remove_rule, { - log_trace("{id}: removing manual slopes row") + log_trace("Slope selector: removing manual slope rule") req(manual_slopes()) selected <- getReactableState("manual_slopes", "selected") req(selected) @@ -135,7 +135,7 @@ manual_slopes_table_server <- function( # Render the manual slopes table (reactable) output$manual_slopes <- renderReactable({ req(manual_slopes()) - log_trace("{id}: rendering slope edit data table") + log_trace(id, ": rendering slope edit data table") isolate({ data <- manual_slopes() }) diff --git a/inst/shiny/modules/tab_nca/setup/parameter_selection.R b/inst/shiny/modules/tab_nca/setup/parameter_selection.R index ea153562f..2cf04120d 100644 --- a/inst/shiny/modules/tab_nca/setup/parameter_selection.R +++ b/inst/shiny/modules/tab_nca/setup/parameter_selection.R @@ -318,6 +318,18 @@ parameter_selection_server <- function(id, processed_pknca_data, parameter_overr ) }) + # Log parameter selection changes (debounced to avoid noise from rapid clicks) + selections_debounced <- debounce(selections_state, 2000) + observeEvent(selections_debounced(), { + state <- selections_debounced() + req(state) + study_types <- study_types_list() + for (st in study_types) { + n_params <- sum(state[[st]], na.rm = TRUE) + log_info("Parameter selection for '", st, "': ", n_params, " parameters selected.") + } + }, ignoreInit = TRUE) + observeEvent(input$clear_all, { state <- selections_state() req(state) diff --git a/inst/shiny/modules/tab_nca/setup/settings.R b/inst/shiny/modules/tab_nca/setup/settings.R index ceda5ae39..08958336d 100644 --- a/inst/shiny/modules/tab_nca/setup/settings.R +++ b/inst/shiny/modules/tab_nca/setup/settings.R @@ -252,6 +252,10 @@ settings_server <- function(id, data, adnca_data, settings_override) { # A guard flag prevents infinite observer loops. updating_filters <- reactiveVal(FALSE) + # Guard to suppress log noise during initial data load / settings restore. + # Set to TRUE after the first analyte cascade completes. + filters_initialized <- reactiveVal(FALSE) + # pending_settings holds the imported settings during the filter cascade. # The analyte observer reads it without clearing; the pcspec observer # consumes it (reads + clears). This ensures the override is available @@ -287,7 +291,14 @@ settings_server <- function(id, data, adnca_data, settings_override) { req(data(), input$select_analyte) if (updating_filters()) return() updating_filters(TRUE) - on.exit(updating_filters(FALSE)) + on.exit({ + updating_filters(FALSE) + if (!filters_initialized()) filters_initialized(TRUE) + }) + + if (filters_initialized()) { + log_info("Analyte selection changed: ", paste(input$select_analyte, collapse = ", ")) + } settings <- pending_settings() @@ -325,6 +336,10 @@ settings_server <- function(id, data, adnca_data, settings_override) { updating_filters(TRUE) on.exit(updating_filters(FALSE)) + if (filters_initialized()) { + log_info("Specimen selection changed: ", paste(input$select_pcspec, collapse = ", ")) + } + settings <- pending_settings() pending_settings(NULL) @@ -347,6 +362,25 @@ settings_server <- function(id, data, adnca_data, settings_override) { .update_profile(settings) }) + # Log profile, method, and min half-life points changes + observeEvent(input$select_profile, { + if (filters_initialized()) { + log_info("NCA profile changed: ", paste(input$select_profile, collapse = ", ")) + } + }, ignoreInit = TRUE) + + observeEvent(input$method, { + if (filters_initialized()) { + log_info("Extrapolation method changed: ", input$method) + } + }, ignoreInit = TRUE) + + observeEvent(input$min_hl_points, { + if (filters_initialized()) { + log_info("Min. half-life points changed: ", input$min_hl_points) + } + }, ignoreInit = TRUE) + # Include keyboard limits for the settings GUI display # Keyboard limits for the setting thresholds @@ -356,6 +390,19 @@ settings_server <- function(id, data, adnca_data, settings_override) { limit_input_value(input, session, "LAMZSPN_threshold", min = 0, lab = "LAMZSPN") limit_input_value(input, session, "min_hl_points", max = 10, min = 2, lab = "Min. HL Points") + # Log flag rule changes + lapply(c("R2ADJ", "R2", "AUCPEO", "AUCPEP", "LAMZSPN"), function(flag) { + rule_id <- paste0(flag, "_rule") + threshold_id <- paste0(flag, "_threshold") + observeEvent(input[[rule_id]], { + state <- if (input[[rule_id]]) "enabled" else "disabled" + log_info("Flag rule ", flag, " ", state) + }, ignoreInit = TRUE) + observeEvent(input[[threshold_id]], { + log_info("Flag rule ", flag, " threshold changed: ", input[[threshold_id]]) + }, ignoreInit = TRUE) + }) + # Reactive value to store the partial intervals data table # Define the parameters that can be used for partial area calculations PARTIAL_INT_PARAMS <- metadata_nca_parameters %>% diff --git a/inst/shiny/modules/tab_nca/setup/slope_selector.R b/inst/shiny/modules/tab_nca/setup/slope_selector.R index 7f3cfe0f1..5c9f2a05c 100644 --- a/inst/shiny/modules/tab_nca/setup/slope_selector.R +++ b/inst/shiny/modules/tab_nca/setup/slope_selector.R @@ -246,6 +246,9 @@ slope_selector_server <- function( # nolint manual_slopes <- slopes_table$manual_slopes refresh_reactable <- slopes_table$refresh_reactable + # Track user-initiated slope changes (buttons, plotly clicks) + user_changed_slopes <- reactiveVal(FALSE) + # Define the click events for the point exclusion and selection in the slope plots last_click_data <- reactiveVal(NULL) observeEvent(event_data("plotly_click", priority = "event"), { @@ -259,6 +262,7 @@ slope_selector_server <- function( # nolint ) # Update reactive values: last click & manual slopes table last_click_data(click_result$last_click_data) + user_changed_slopes(TRUE) manual_slopes(click_result$manual_slopes) # render rectable anew # @@ -266,12 +270,25 @@ slope_selector_server <- function( # nolint refresh_reactable(refresh_reactable() + 1) }) + # Also flag user-initiated changes from add/remove buttons + observeEvent(slopes_table$refresh_reactable(), { + user_changed_slopes(TRUE) + }, ignoreInit = TRUE) + #' Separate event handling updating displayed reactable upon every change (adding and removing #' rows, plots selection, edits). This needs to be separate call, since simply re-rendering #' the table would mean losing focus on text inputs when entering values. observeEvent(manual_slopes(), { req(manual_slopes()) + if (user_changed_slopes()) { + n_rules <- nrow(manual_slopes()) + n_excl <- sum(manual_slopes()$TYPE == "Exclusion", na.rm = TRUE) + n_incl <- n_rules - n_excl + log_info("Slope rules updated: ", n_rules, " total (", n_incl, " selections, ", n_excl, " exclusions)") + user_changed_slopes(FALSE) + } + # Update reactable with rules reactable::updateReactable( outputId = "manual_slopes", diff --git a/inst/shiny/modules/tab_nca/zip.R b/inst/shiny/modules/tab_nca/zip.R index 5334e49ac..2dd18d40e 100644 --- a/inst/shiny/modules/tab_nca/zip.R +++ b/inst/shiny/modules/tab_nca/zip.R @@ -759,7 +759,7 @@ zip_server <- function(id, res_nca, adnca_data, settings, grouping_vars) { } items$extras <- TREE_LIST$extras } else { - items$extras <- TREE_LIST$extras[c("settings_file", "session_info")] + items$extras <- TREE_LIST$extras[c("settings_file", "session_info", "session_log")] } items @@ -796,7 +796,8 @@ TREE_LIST <- list( results_slides = "", r_script = "", settings_file = "", - session_info = "" + session_info = "", + session_log = "" ) ) diff --git a/pkgdown/_pkgdown.yml b/pkgdown/_pkgdown.yml index c42ff1cdd..d980799f2 100644 --- a/pkgdown/_pkgdown.yml +++ b/pkgdown/_pkgdown.yml @@ -33,6 +33,8 @@ navbar: href: articles/adding_tlg.html - text: Design & Architecture href: articles/design.html + - text: Session Log Reference + href: articles/session_log.html - text: Functions manual href: reference/index.html project: diff --git a/vignettes/session_log.Rmd b/vignettes/session_log.Rmd new file mode 100644 index 000000000..7e0d08546 --- /dev/null +++ b/vignettes/session_log.Rmd @@ -0,0 +1,200 @@ +--- +title: "Session Log Reference" +description: > + Reference for the session log exported with aNCA results. + Describes what events are captured, log levels, and configuration. +output: rmarkdown::html_vignette +vignette: > + %\VignetteIndexEntry{Session Log Reference} + %\VignetteEngine{knitr::knitr} + %\VignetteEncoding{UTF-8} +--- + +When you export results as a ZIP file, aNCA includes a `session_log.txt` that +records application events from your session. This page explains what the log +contains, how to read it, and how to configure its verbosity. + +## What the log captures + +The session log records events emitted by explicit logging calls placed +throughout the aNCA Shiny application. Each line follows this format: + +``` +[2025-04-30 09:40:16] INFO: Calculating NCA results... +``` + +The log captures **application-level events** — not raw R console output. +Only messages that pass through aNCA's internal `log_*()` functions appear +in the file. + +### What is NOT captured + +- Unexpected R warnings or errors that are not wrapped in a `tryCatch` block + with a corresponding `log_warn()` or `log_error()` call. +- Messages from third-party packages (e.g., `PKNCA`, `dplyr`) unless the + application explicitly catches and logs them. +- Raw `cat()` or `print()` output from any source. + +## Log levels + +aNCA uses six log levels, ordered from most to least verbose: + +| Level | Purpose | +|---|---| +| **TRACE** | Fine-grained internal operations (module attachment, reactive updates) | +| **DEBUG** | Detailed diagnostic data (parameter lists, slope rule dumps) | +| **INFO** | Key user-facing operations (data upload, NCA calculation, setting changes) | +| **SUCCESS** | Completion confirmations (data loaded, results calculated) | +| **WARN** | Non-fatal issues (calculation warnings, incompatible settings) | +| **ERROR** | Failures (calculation errors, file load failures) | + +The default threshold is **INFO**, meaning only INFO, SUCCESS, WARN, and ERROR +messages appear. Set `aNCA_LOG_LEVEL=TRACE` or `aNCA_LOG_LEVEL=DEBUG` in your +`.Renviron` file to see more detail. + +## Logged events by workflow stage + +The following table lists all events currently captured in the session log, +organized by the stage of the workflow where they occur. + +### Startup + +| Event | Level | +|---|---| +| Application startup | INFO | +| Application restarting | INFO | + +### Data Upload + +| Event | Level | +|---|---| +| Data upload initialized (with file path) | INFO | +| All user data loaded | SUCCESS | +| File loading errors | ERROR | +| User data binding errors | ERROR | + +### Settings Restore + +| Event | Level | +|---|---| +| Settings restored from version | SUCCESS | +| Settings loaded from file | SUCCESS | +| Settings load failure | ERROR | +| Incompatible settings (analyte, profile, specimen) | WARN | + +### Data Mapping + +| Event | Level | +|---|---| +| Processing data mapping | INFO | +| Data mapping warnings | WARN | +| Data mapping errors | ERROR | + +### Data Filtering + +| Event | Level | +|---|---| +| Data filtering warnings | WARN | + +### NCA Settings + +| Event | Level | +|---|---| +| Analyte selection changed | INFO | +| Specimen selection changed | INFO | +| NCA profile selection changed | INFO | +| Extrapolation method changed | INFO | +| Min. half-life points changed | INFO | +| Flag rule enabled/disabled | INFO | +| Flag rule threshold changed | INFO | +| BLQ imputation strategy changed | INFO | +| Start concentration imputation toggled | INFO | + +### Parameter Selection + +| Event | Level | +|---|---| +| Parameter count per study type | INFO | + +### Slope Selector + +| Event | Level | +|---|---| +| Module server attachment | TRACE | +| Plotly click detected | TRACE | +| Manual slope rule added | INFO | +| Manual slope rule removed | INFO | +| Slope rules summary (inclusions/exclusions count) | DEBUG | +| Manual slopes override applied | DEBUG | +| Manual slopes override incompatible | WARN | +| Slope edit table rendering | TRACE | + +### General Exclusions + +| Event | Level | +|---|---| +| Exclusion added (row count, type, reason) | INFO | +| Exclusion removed | INFO | +| Exclusions restored from settings | INFO | + +### NCA Calculation + +| Event | Level | +|---|---| +| Creating / updating PKNCA data object | TRACE | +| PKNCA data object created | SUCCESS | +| PKNCA data object creation error | ERROR | +| Updating parameter selection data | TRACE | +| Calculating NCA results | INFO | +| NCA calculation warnings | WARN | +| NCA results calculated | SUCCESS | +| NCA calculation error | ERROR | +| Invalid parameters | ERROR | + +### Exploration Plots + +| Event | Level | +|---|---| +| Rendering individual plots | INFO | +| Computing mean plot | INFO | +| Rendering boxplot | INFO | +| Exploration plot saved (with overwrite flag) | INFO | +| Exploration plot removed | INFO | + +### TLG Generation + +| Event | Level | +|---|---| +| TLG module server attachment | TRACE | +| Submitted TLG list | DEBUG | +| TLG list rendering error | ERROR | + +### Export + +| Event | Level | +|---|---| +| Downloading summary statistics as CSV | INFO | + +### Units & Other + +| Event | Level | +|---|---| +| Applying custom units specification | TRACE | + +## Configuring the log level + +Set the `aNCA_LOG_LEVEL` environment variable before starting the app. +Valid values: `TRACE`, `DEBUG`, `INFO` (default), `SUCCESS`, `WARN`, `ERROR`. + +In your `.Renviron` file: + +``` +aNCA_LOG_LEVEL=DEBUG +``` + +Or set it in R before launching: + +```r +Sys.setenv(aNCA_LOG_LEVEL = "TRACE") +aNCA::run_app() +```