diff --git a/.github/workflows/test-coverage.yaml b/.github/workflows/test-coverage.yaml deleted file mode 100644 index 2c5bb50..0000000 --- a/.github/workflows/test-coverage.yaml +++ /dev/null @@ -1,50 +0,0 @@ -# Workflow derived from https://github.com/r-lib/actions/tree/v2/examples -# Need help debugging build failures? Start at https://github.com/r-lib/actions#where-to-find-help -on: - push: - branches: [main, master] - pull_request: - branches: [main, master] - -name: test-coverage - -jobs: - test-coverage: - runs-on: ubuntu-latest - env: - GITHUB_PAT: ${{ secrets.GITHUB_TOKEN }} - - steps: - - uses: actions/checkout@v3 - - - uses: r-lib/actions/setup-r@v2 - with: - use-public-rspm: true - - - uses: r-lib/actions/setup-r-dependencies@v2 - with: - extra-packages: any::covr - needs: coverage - - - name: Test coverage - run: | - covr::codecov( - quiet = FALSE, - clean = FALSE, - install_path = file.path(Sys.getenv("RUNNER_TEMP"), "package") - ) - shell: Rscript {0} - - - name: Show testthat output - if: always() - run: | - ## -------------------------------------------------------------------- - find ${{ runner.temp }}/package -name 'testthat.Rout*' -exec cat '{}' \; || true - shell: bash - - - name: Upload test results - if: failure() - uses: actions/upload-artifact@v3 - with: - name: coverage-test-failures - path: ${{ runner.temp }}/package diff --git a/.lintr b/.lintr index 28b31d1..7f3d75a 100644 --- a/.lintr +++ b/.lintr @@ -1,4 +1,4 @@ -linters: with_defaults(line_length_linter(90)) +linters: linters_with_defaults (line_length_linter(90)) exclude: "# Exclude Linting" exclude_start: "# Begin Exclude Linting" exclude_end: "# End Exclude Linting" diff --git a/DESCRIPTION b/DESCRIPTION index 90d9472..58c8c44 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -1,6 +1,6 @@ Package: shinyusertracking Title: Add user tracking to Shiny apps -Version: 1.0.0.000 +Version: 1.0.1.000 Authors@R: person( "Mark", "McPherson", , @@ -11,13 +11,8 @@ Description: Log session and user data to a Google sheet. License: MIT + file LICENSE Encoding: UTF-8 Roxygen: list(markdown = TRUE) -RoxygenNote: 7.2.2 -Suggests: - covr, - testthat (>= 3.0.0) +RoxygenNote: 7.3.1 Config/testthat/edition: 3 Imports: - shiny, googlesheets4, - hms, lubridate diff --git a/R/usertracking.R b/R/usertracking.R index f7bfaa1..0ea8b6e 100644 --- a/R/usertracking.R +++ b/R/usertracking.R @@ -1,11 +1,10 @@ - #' Add user tracking #' #' Log session ID, username (only for Private apps), session start, end and #' duration to a Google sheet. #' -#' @param google_email Email used for Google account username. -#' @param sheet_id Google sheet ID. +#' @param columns Which columns to record, from id, username, login, logout and +#' duration. By default all will be recorded. #' @param session Shiny session object. #' #' @return Nothing; used for side effect. @@ -18,8 +17,7 @@ #' ui <- fluidPage() #' server <- function(input, output, session) { #' shinyusertracking::set_user_tracking( -#' "joe.bloggs@google.com", -#' "1234567890987654321", +#' c("login", "logout", "duration"), #' session #' ) #' } @@ -27,9 +25,41 @@ #' shinyApp(ui, server) #' } #' -set_user_tracking <- function(google_email, sheet_id, session) { - if (sheet_id == "") { - return(invisible()) +set_user_tracking <- function(columns = NULL, session) { + known_cols <- c( + "id", + "username", + "login", + "logout", + "duration" + ) + + if (is.null(columns)) { + columns <- known_cols + } else { + stopifnot({ + columns %in% known_cols + }) + } + + eval_lines(".google-sheets-credentials") + + google_email <- NULL + sheet_id <- NULL + + try({ + google_email <- get("GOOGLE_SHEET_USER") + }) + try({ + sheet_id <- get("GOOGLE_SHEET_ID") + }) + + if (is.null(google_email) || is.null(sheet_id)) { + warning( + "Credentials missing for shinyusertracking::set_user_tracking", + call. = FALSE + ) + return() } googlesheets4::gs4_auth( @@ -37,34 +67,67 @@ set_user_tracking <- function(google_email, sheet_id, session) { cache = ".secret/" ) - shiny::isolate({ - userdata <<- userdata <- data.frame( # Exclude Linting - id = session$token, - username = ifelse(is.null(session$user), "unknown", session$user), - login = Sys.time(), - logout = lubridate::NA_POSIXct_, - duration = NA_character_ - ) - }) + session$userData$tracking <- data.frame( + id = session$token, + username = ifelse(is.null(session$user), "unknown", session$user), + login = Sys.time(), + logout = lubridate::NA_POSIXct_, + duration = NA_character_ + ) session$onSessionEnded(function() { - shiny::isolate({ - userdata[userdata$id == session$token, "logout"] <- Sys.time() - userdata[userdata$id == session$token, "duration"] <- as.character( - hms::hms( - round( - lubridate::as.period( - userdata[userdata$id == session$token, "logout"] - - userdata[userdata$id == session$token, "login"], - "seconds" - ) - ) - ) - ) + session$userData$tracking$logout <- Sys.time() - googlesheets4::sheet_append(sheet_id, userdata) - }) + duration <- difftime( + session$userData$tracking$logout, + session$userData$tracking$login, + units = "secs" + ) + duration <- abs(as.numeric(duration)) + duration <- sprintf( + "%02d:%02d:%02d", # hh:mm:ss + duration %/% 3600, # whole hours (could be > 24) + duration %% 3600 %/% 60, # whole minutes left + duration %% 60 %/% 1 + round(duration %% 60 %% 1) # rounded seconds left + ) + + session$userData$tracking$duration <- as.character(duration) + + googlesheets4::sheet_append( + sheet_id, + subset(session$userData$tracking, select = columns) + ) }) +} + + +#' Evaluate each line of plain text file +#' +#' Reads a plain text file line by line, evaluating each line. Useful for +#' creating variables dynamically, e.g. reading in parameters. +#' +#' @param filepath Filepath as a String. +#' @param envir Environment to evaluate in. Default is calling environment. +#' +#' @return Nothing +#' +#' @examples +#' \dontrun{ +#' filepath <- tempfile() +#' writeLines( +#' text = "LEFT = \"right\"", +#' con = filepath +#' ) +#' eval_lines(filepath) +#' print(LEFT) +#' unlink(filepath) # delete temporary file +#' rm(left) # remove example variable +#' } +eval_lines <- function(filepath, envir = parent.frame()) { + con <- file(filepath, open = "r") + on.exit(close(con)) - invisible() + while (length(line <- readLines(con, n = 1, warn = FALSE)) > 0) { + eval(parse(text = line), envir = envir) + } } diff --git a/README.md b/README.md index 11b4114..51ef9d7 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,6 @@ # shinyusertracking -[![Codecov test coverage](https://codecov.io/gh/MarkMc1089/shinyusertracking/branch/master/graph/badge.svg)](https://app.codecov.io/gh/MarkMc1089/shinyusertracking?branch=master) [![R-CMD-check](https://github.com/MarkMc1089/shinyusertracking/actions/workflows/R-CMD-check.yaml/badge.svg)](https://github.com/MarkMc1089/shinyusertracking/actions/workflows/R-CMD-check.yaml) @@ -19,7 +18,7 @@ devtools::install_github("nhsbsa-data-analytics/shinyusertracking") ## Example -Just add the function at the top of your `server` code. You will need to provide the ID of a Google Sheet and the username (email) of the Google account it is in. +Just add the function at the top of your `server` code. You will need to provide the ID of a Google Sheet and the username (email) of the Google account it is in. ``` r library(shiny) @@ -28,11 +27,26 @@ ui <- fluidPage() server <- function(input, output, session) { shinyusertracking::set_user_tracking( - "joe.bloggs@google.com", - "1234567890987654321", session ) } shinyApp(ui, server) ``` + +Optionally, you can choose to log specific columns only. + +Column|Description +:---:|:---: +id|The Shiny session ID +username|The username of user, if available (`null` if app is public) +login|Timestamp of session start +logout|Timestamp of session end +duration|Duration of session in `hh:mm:ss` format + +``` r +shinyusertracking::set_user_tracking( + columns = c("login", "logout", "duration"), + session +) +``` diff --git a/codecov.yml b/codecov.yml deleted file mode 100644 index 04c5585..0000000 --- a/codecov.yml +++ /dev/null @@ -1,14 +0,0 @@ -comment: false - -coverage: - status: - project: - default: - target: auto - threshold: 1% - informational: true - patch: - default: - target: auto - threshold: 1% - informational: true diff --git a/man/eval_lines.Rd b/man/eval_lines.Rd new file mode 100644 index 0000000..337e68e --- /dev/null +++ b/man/eval_lines.Rd @@ -0,0 +1,33 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/usertracking.R +\name{eval_lines} +\alias{eval_lines} +\title{Evaluate each line of plain text file} +\usage{ +eval_lines(filepath, envir = parent.frame()) +} +\arguments{ +\item{filepath}{Filepath as a String.} + +\item{envir}{Environment to evaluate in. Default is calling environment.} +} +\value{ +Nothing +} +\description{ +Reads a plain text file line by line, evaluating each line. Useful for +creating variables dynamically, e.g. reading in parameters. +} +\examples{ +\dontrun{ +filepath <- tempfile() +writeLines( + text = "LEFT = \"right\"", + con = filepath +) +eval_lines(filepath) +print(LEFT) +unlink(filepath) # delete temporary file +rm(left) # remove example variable +} +} diff --git a/man/set_user_tracking.Rd b/man/set_user_tracking.Rd index b152b87..54d017d 100644 --- a/man/set_user_tracking.Rd +++ b/man/set_user_tracking.Rd @@ -4,12 +4,11 @@ \alias{set_user_tracking} \title{Add user tracking} \usage{ -set_user_tracking(google_email, sheet_id, session) +set_user_tracking(columns = NULL, session) } \arguments{ -\item{google_email}{Email used for Google account username.} - -\item{sheet_id}{Google sheet ID.} +\item{columns}{Which columns to record, from id, username, login, logout and +duration. By default all will be recorded.} \item{session}{Shiny session object.} } @@ -27,8 +26,7 @@ library(shiny) ui <- fluidPage() server <- function(input, output, session) { shinyusertracking::set_user_tracking( - "joe.bloggs@google.com", - "1234567890987654321", + c("login", "logout", "duration"), session ) } diff --git a/tests/testthat.R b/tests/testthat.R deleted file mode 100644 index 638376b..0000000 --- a/tests/testthat.R +++ /dev/null @@ -1,12 +0,0 @@ -# This file is part of the standard setup for testthat. -# It is recommended that you do not modify it. -# -# Where should you do additional test configuration? -# Learn more about the roles of various files in: -# * https://r-pkgs.org/tests.html -# * https://testthat.r-lib.org/reference/test_package.html#special-files - -library(testthat) -library(shinyusertracking) - -test_check("shinyusertracking") diff --git a/tests/testthat/test-usertracking.R b/tests/testthat/test-usertracking.R deleted file mode 100644 index 8849056..0000000 --- a/tests/testthat/test-usertracking.R +++ /dev/null @@ -1,3 +0,0 @@ -test_that("multiplication works", { - expect_equal(2 * 2, 4) -})