Skip to content

Commit

Permalink
Merge pull request #2 from nhsbsa-data-analytics/better-process
Browse files Browse the repository at this point in the history
Better process
  • Loading branch information
MarkMc1089 authored Nov 8, 2024
2 parents 877d389 + 5cb4156 commit 4db6363
Show file tree
Hide file tree
Showing 14 changed files with 398 additions and 173 deletions.
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,4 @@
.Rproj.user
.Rhistory
.google-sheets-credentials
.secret
4 changes: 2 additions & 2 deletions .google-sheets-credentials.example
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
GOOGLE_SHEET_ID = "sheetid"
GOOGLE_SHEET_USER = "[email protected]"
GOOGLE_SHEET_ID=sheetid
GOOGLE_SHEET_USER=[email protected]
12 changes: 10 additions & 2 deletions DESCRIPTION
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
Package: shinyusertracking
Title: Add user tracking to Shiny apps
Version: 1.0.1.000
Version: 1.0.2.000
Authors@R:
person(
"Mark", "McPherson", ,
Expand All @@ -13,6 +13,14 @@ Encoding: UTF-8
Roxygen: list(markdown = TRUE)
RoxygenNote: 7.3.1
Config/testthat/edition: 3
VignetteBuilder: knitr
Imports:
googlesheets4,
lubridate
lubridate,
pkgload,
rstudioapi,
shiny,
usethis
Suggests:
knitr,
rmarkdown
5 changes: 4 additions & 1 deletion NAMESPACE
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
# Generated by roxygen2: do not edit by hand

export(set_user_tracking)
export(add_credentials)
export(set_credentials)
export(setup_sheet)
export(use_logging)
243 changes: 172 additions & 71 deletions R/usertracking.R
Original file line number Diff line number Diff line change
@@ -1,31 +1,96 @@
#' Add user tracking
#' Securely ask for Google sheet ID and username
#'
#' Log session ID, username (only for Private apps), session start, end and
#' duration to a Google sheet.
#' Uses Rstudio secret functionality together with the keyring package to securely
#' store the input values. If keyring is not installed and up to date, the option to
#' do so will be given. Once keyring is present, tick the box to save the secrets.
#' The next time this is run, the secrets will be pre-filled.
#'
#' @param columns Which columns to record, from id, username, login, logout and
#' duration. By default all will be recorded.
#' @param session Shiny session object.
#' @param file File used to store credentials. Defaults to `.google-sheets-credentials`.
#' @param overwrite Whether to overwrite file if it already exists. Default is FALSE.
#'
#' @return Nothing, used for side-effects only
#'
#' @return Nothing; used for side effect.
#' @export
#'
#' @examples
#' \dontrun{
#' library(shiny)
#'
#' ui <- fluidPage()
#' server <- function(input, output, session) {
#' shinyusertracking::set_user_tracking(
#' c("login", "logout", "duration"),
#' session
#' )
#' add_credentials(overwrite = TRUE)
#' }
add_credentials <- function(file = ".google-sheets-credentials", overwrite = FALSE) {
if (!overwrite && file.exists(file)) {
stop(
"Credentials file ", file, " already exists; set overwrite = TRUE if you are sure."
)
}

writeLines(
c(
paste0(
"GOOGLE_SHEET_ID=",
tryCatch(
rstudioapi::askForSecret("GOOGLE_SHEET_ID"),
error = \(e) stop("Aborting...")
)
),
paste0(
"GOOGLE_SHEET_USER=",
tryCatch(
rstudioapi::askForSecret("GOOGLE_SHEET_USER"),
error = \(e) stop("Aborting...")
)
)
),
file
)

usethis::use_git_ignore(file)
}


#' Set environment variables for the Google sheet ID and username
#'
#' shinyApp(ui, server)
#' @param file File used to store credentials. Defaults to `.google-sheets-credentials`.
#'
#' @return Nothing, used for side-effects only
#'
#' @export
#'
#' @examples
#' \dontrun{
#' set_credentials()
#' }
set_credentials <- function(file = ".google-sheets-credentials") {
if (file.exists(file)) {
tryCatch(
lines <- readLines(file),
error = \(e) stop("Error reading lines from ", file)
)
} else {
stop("File ", file, " does not exist")
}

id <- "GOOGLE_SHEET_ID"
id_line <- lines[startsWith(lines, id)]
if (!length(id_line)) stop("No value for ", id, " in ", file)

user <- "GOOGLE_SHEET_USER"
user_line <- lines[startsWith(lines, user)]
if (!length(user_line)) stop("No value for ", user, " in ", file)

Sys.setenv(GOOGLE_SHEET_ID = gsub("^[^\\=]*\\=(.*)$", "\\1", id_line))
Sys.setenv(GOOGLE_SHEET_USER = gsub("^[^\\=]*\\=(.*)$", "\\1", user_line))
}


#' Check that only known columns are provided
#'
#' @param columns Either NULL or vector of column names.
#'
#' @return The provided columns, if all are known; all known columns if NULL input;
#' or error if some provided columns are not known.
#'
set_user_tracking <- function(columns = NULL, session) {
#' @noRd
check_cols <- function(columns) {
known_cols <- c(
"id",
"username",
Expand All @@ -35,35 +100,102 @@ set_user_tracking <- function(columns = NULL, session) {
)

if (is.null(columns)) {
columns <- known_cols
return(known_cols)
} else {
stopifnot({
columns %in% known_cols
})
stopifnot(
"Columns not in: id, username, login, logout, duration" = columns %in% known_cols
)
}

eval_lines(".google-sheets-credentials")
columns
}

google_email <- NULL
sheet_id <- NULL

try({
google_email <- get("GOOGLE_SHEET_USER")
})
try({
sheet_id <- get("GOOGLE_SHEET_ID")
})
#' Add a new sheet for tracking to the Google sheets
#'
#' @param sheet_name Name for the sheet. Default is to use current package name.
#' @param columns Which columns to log, from id, username, login, logout and
#' duration. By default login, logout and duration will be logged.
#' @param creds File used to store credentials. Defaults to `.google-sheets-credentials`.
#'
#' @return Nothing, used for side-effects only
#'
#' @export
#'
#' @examples
#' \dontrun{
#' setup_sheet("A new Shiny app", c("login", "logout", "duration"))
#' }
setup_sheet <- function(sheet_name = pkgload::pkg_name(),
columns = c("login", "logout", "duration"),
creds = ".google-sheets-credentials") {
columns <- check_cols(columns)

if (is.null(google_email) || is.null(sheet_id)) {
warning(
"Credentials missing for shinyusertracking::set_user_tracking",
call. = FALSE
set_credentials(creds)

googlesheets4::gs4_auth(
email = Sys.getenv("GOOGLE_SHEET_USER"),
cache = ".secret/"
)

usethis::use_git_ignore(".secret")

googlesheets4::sheet_add(Sys.getenv("GOOGLE_SHEET_ID"), sheet_name)
googlesheets4::sheet_append(
Sys.getenv("GOOGLE_SHEET_ID"),
data.frame(matrix(columns, nrow = 1)),
sheet_name
)
}


#' Add visit tracking to Shiny app
#'
#' Log session ID, username (only for Private apps), session start, end and
#' duration to a Google sheet.
#'
#' @param sheet_name Name for the sheet. Default is to use current package name.
#' @param columns Which columns to log, from id, username, login, logout and
#' duration. By default login, logout and duration will be logged.
#' @param creds File used to store credentials. Defaults to `.google-sheets-credentials`.
#'
#' @return Nothing, used for side-effects only
#'
#' @export
#'
#' @examples
#' \dontrun{
#' library(shiny)
#'
#' ui <- fluidPage()
#' server <- function(input, output, session) {
#' shinyusertracking::set_user_tracking(
#' c("login", "logout", "duration"),
#' session
#' )
#' }
#'
#' shinyApp(ui, server)
#' }
use_logging <- function(sheet_name = pkgload::pkg_name(),
columns = c("login", "logout", "duration"),
creds = ".google-sheets-credentials") {
columns <- check_cols(columns)

stopifnot(
"set_user_tracking can run only in a Shiny app" = shiny::isRunning(),
"set_user_tracking requires a Shiny session object to run" = exists(
"session",
parent.frame()
)
return()
}
)

session <- get("session", parent.frame())

set_credentials(creds)

googlesheets4::gs4_auth(
email = google_email,
email = Sys.getenv("GOOGLE_SHEET_USER"),
cache = ".secret/"
)

Expand Down Expand Up @@ -94,40 +226,9 @@ set_user_tracking <- function(columns = NULL, session) {
session$userData$tracking$duration <- as.character(duration)

googlesheets4::sheet_append(
sheet_id,
subset(session$userData$tracking, select = columns)
Sys.getenv("GOOGLE_SHEET_ID"),
subset(session$userData$tracking, select = columns),
sheet_name
)
})
}


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

while (length(line <- readLines(con, n = 1, warn = FALSE)) > 0) {
eval(parse(text = line), envir = envir)
}
}
38 changes: 11 additions & 27 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,14 @@

## Installation

You can install `shinyusertracking` from [GitHub](https://github.com/) with:
To install this package from GitHub, use the below code. Note that you must explicitly ask for vignettes to be built when installing from GitHub.

``` r
# install.packages("devtools")
devtools::install_github("nhsbsa-data-analytics/shinyusertracking")
```
`remotes::install_github("nhsbsa-data-analytics/shinyusertracking", build_vignettes = TRUE)`

## Usage

Fields available for logging are:

Column|Description
:---:|:---:
id|The Shiny session ID
Expand All @@ -26,39 +25,24 @@ login|Timestamp of session start
logout|Timestamp of session end
duration|Duration of session in `hh:mm:ss` format

1. Create a new Google sheet, with column headers corresponding to the columns you will be recording (they can be named differently to the columns given if you want).
2. Copy the contents of the file `.google-sheets-credentials.example` in this repo to a file named `.google-sheets-credentials` in the root directory of your app.
3. IMPORTANT: You must tell git to ignore this! `usethis::use_git_ignore(".google-sheets-credentials")`.
4. IMPORTANT: While you are at it, also `usethis::use_git_ignore(".secret/")`. This is the directory in which the Google authorisation secret will be stored.
5. Replace the example for `GOOGLE_SHEET_ID` with the ID of the Google sheet. You can find this in the URL. For example, if the URL is `https://docs.google.com/spreadsheets/d/1vwrKiwX4T_-A2IldWnjcd1PHlCDsGAq9U-yTtQ6tgzk/edit?gid=0#gid=0`, the ID is `1vwrKiwX4T_-A2IldWnjcd1PHlCDsGAq9U-yTtQ6tgzk`.
6. Replace the example for `GOOGLE_SHEET_USER` with the Google account username. See the page _Coding and Dashboards/ R code/Shiny app visit tracking_ in the DALL Wiki for details of a Data Science team Google account to use.
7. Add the code at the top of your `server` function.
8. The first time it is run, you will be asked to authenticate the Google account access. Once this is done, an authorisation token will be stored in the `.secrets` directory. This can be reused when adding visit tracking for another app. So alternatively, you could copy an existing `.secret` directory, with the token inside, and paste into the root directory of your app. Note that you will not be able to confirm the access on AVDs, as the Google pages to do so are not whitelisted. See the page _Coding and Dashboards/ R code/Shiny app visit tracking_ in the DALL Wiki for details of where to find an existing token.
8. Ensure you bundle both the `.google-sheets-credentials` file and the `.secret` directory when your app is deployed.
By default, `login`, `logout` and `duration` will be logged. Although `sessionid` and `username` are also available, these have potential to be treated as PII, so please ensure you meet any legal obligations if logging these.

Note that 'visits' when running it locally in development will also track. So you might want to introduce some configuration to only run in production. Alternatively, try to remember to delete any entries logged during development from the Google sheet.
For instructions on using check out the vignette by running `vignette("adding-logging-to-a-shiny-app", "shinyusertracking")`.

Note that 'visits' when running it locally in development will also be logged. So you might want to introduce some configuration to only run in production. Alternatively, try to remember to delete any entries logged during development from the Google sheet.

## Example

Once the necessary credentials are in place, and a Google sheet is ready to be logged to, just place the a call to `use_logging()` at the top of your server function.

``` r
library(shiny)

ui <- fluidPage()

server <- function(input, output, session) {
shinyusertracking::set_user_tracking(
session
)
shinyusertracking::use_logging()
}

shinyApp(ui, server)
```

Optionally, you can choose to log specific columns only.

``` r
shinyusertracking::set_user_tracking(
columns = c("login", "logout", "duration"),
session
)
```
Loading

0 comments on commit 4db6363

Please sign in to comment.