From 99e5a1014fb30a37a915f9981735c8b35e38f0d0 Mon Sep 17 00:00:00 2001 From: Toph Allen Date: Tue, 29 Apr 2025 11:29:23 -0400 Subject: [PATCH 01/23] initial implementation of hits endpoint --- R/connect.R | 10 ++++++++++ R/get.R | 15 +++++++++++++++ R/ptype.R | 7 +++++++ 3 files changed, 32 insertions(+) diff --git a/R/connect.R b/R/connect.R index 77059597..989d9966 100644 --- a/R/connect.R +++ b/R/connect.R @@ -770,6 +770,16 @@ Connect <- R6::R6Class( self$GET(path, query = query) }, + inst_content_hits = function(from = NULL, to = NULL) { + self$GET( + v1_url("instrumentation", "content", "hits"), + query = list( + from = from, + to = to + ) + ) + } + #' @description Get running processes. procs = function() { warn_experimental("procs") diff --git a/R/get.R b/R/get.R index b19facdb..7c56f39f 100644 --- a/R/get.R +++ b/R/get.R @@ -479,6 +479,21 @@ get_usage_static <- function(src, content_guid = NULL, } +get_usage <- function(client, from = NULL, to = NULL) { + from <- format(from, "%Y-%m-%dT%H:%M:%SZ") + to <- format(to, "%Y-%m-%dT%H:%M:%SZ") + usage_raw <- client$GET( + connectapi:::v1_url("instrumentation", "content", "hits"), + query = list( + from = from, + to = to + ) + ) + + parse_connectapi_typed(usage_raw, connectapi_ptypes$usage) +} + + #' Get Audit Logs from Posit Connect Server #' #' @param src The source object diff --git a/R/ptype.R b/R/ptype.R index 030f8321..b83b8277 100644 --- a/R/ptype.R +++ b/R/ptype.R @@ -38,6 +38,13 @@ connectapi_ptypes <- list( "bundle_id" = NA_character_, "data_version" = NA_integer_ ), + usage = tibble::tibble( + "id" = NA_integer_, + "user_guid" = NA_character_, + "content_guid" = NA_character_, + "timestamp" = NA_datetime_, + "data" = NA_list_ + ), content = tibble::tibble( "guid" = NA_character_, "name" = NA_character_, From 1e6a8161ff1fb58aa157eea001a2ebee0419fc00 Mon Sep 17 00:00:00 2001 From: Toph Allen Date: Wed, 30 Apr 2025 13:19:51 -0400 Subject: [PATCH 02/23] use client method --- NEWS.md | 5 +++++ R/connect.R | 2 +- R/get.R | 9 ++------- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/NEWS.md b/NEWS.md index ad03bd63..44305032 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,5 +1,10 @@ # connectapi (development version) +## New features + +- New `get_usage()` function returns content usage data from Connect's `GET + v1/instrumentation/content/hits` endpoint. + # connectapi 0.7.0 ## New features diff --git a/R/connect.R b/R/connect.R index 989d9966..309bc84a 100644 --- a/R/connect.R +++ b/R/connect.R @@ -778,7 +778,7 @@ Connect <- R6::R6Class( to = to ) ) - } + }, #' @description Get running processes. procs = function() { diff --git a/R/get.R b/R/get.R index 7c56f39f..a7d747bd 100644 --- a/R/get.R +++ b/R/get.R @@ -482,13 +482,8 @@ get_usage_static <- function(src, content_guid = NULL, get_usage <- function(client, from = NULL, to = NULL) { from <- format(from, "%Y-%m-%dT%H:%M:%SZ") to <- format(to, "%Y-%m-%dT%H:%M:%SZ") - usage_raw <- client$GET( - connectapi:::v1_url("instrumentation", "content", "hits"), - query = list( - from = from, - to = to - ) - ) + + usage_raw <- client$inst_content_hits(from, to) parse_connectapi_typed(usage_raw, connectapi_ptypes$usage) } From 437803b5994ce102c4ddbe95cada643f21825b99 Mon Sep 17 00:00:00 2001 From: Toph Allen Date: Fri, 2 May 2025 11:40:23 -0400 Subject: [PATCH 03/23] complete get_usage functionality --- NAMESPACE | 1 + NEWS.md | 3 +- R/connect.R | 21 ++++++++++-- R/get.R | 67 +++++++++++++++++++++++++++++++++---- R/parse.R | 57 +++++++++++++++++++++++++++++++ man/PositConnect.Rd | 22 ++++++++++++ man/get_usage.Rd | 66 ++++++++++++++++++++++++++++++++++++ tests/testthat/test-get.R | 15 +++++++-- tests/testthat/test-parse.R | 46 +++++++++++++++++++++++++ 9 files changed, 286 insertions(+), 12 deletions(-) create mode 100644 man/get_usage.Rd diff --git a/NAMESPACE b/NAMESPACE index 8a647e87..ea7e7be3 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -98,6 +98,7 @@ export(get_tag_data) export(get_tags) export(get_thumbnail) export(get_timezones) +export(get_usage) export(get_usage_shiny) export(get_usage_static) export(get_user_permission) diff --git a/NEWS.md b/NEWS.md index 1329f55c..bdd05835 100644 --- a/NEWS.md +++ b/NEWS.md @@ -3,7 +3,8 @@ ## New features - New `get_usage()` function returns content usage data from Connect's `GET - v1/instrumentation/content/hits` endpoint. + v1/instrumentation/content/hits` endpoint on Connect v2025.04.0 and higher. + (#390) ## Enhancements and fixes diff --git a/R/connect.R b/R/connect.R index 624b51fd..cf14b194 100644 --- a/R/connect.R +++ b/R/connect.R @@ -818,12 +818,29 @@ Connect <- R6::R6Class( self$GET(path, query = query) }, + #' @description Get content usage data. + #' @param from Optional `Date` or `POSIXt`; start of the time window. If a + #' `Date`, coerced to `YYYY-MM-DDT00:00:00` in the caller's time zone. + #' @param to Optional `Date` or `POSIXt`; end of the time window. If a + #' `Date`, coerced to `YYYY-MM-DDT23:59:59` in the caller's time zone. inst_content_hits = function(from = NULL, to = NULL) { + error_if_less_than(client$version, "2025.04.0") + + # If this is called with date objects with no timestamp attached, it's + # reasonable to assume that the caller is indicating the days as an + # inclusive range. + if (inherits(from, "Date")) { + from <- as.POSIXct(paste(from, "00:00:00"), tz = "") + } + if (inherits(to, "Date")) { + to <- as.POSIXct(paste(to, "23:59:59"), tz = "") + } + self$GET( v1_url("instrumentation", "content", "hits"), query = list( - from = from, - to = to + from = make_timestamp(from), + to = make_timestamp(to) ) ) }, diff --git a/R/get.R b/R/get.R index bb0a638f..1ba4051f 100644 --- a/R/get.R +++ b/R/get.R @@ -526,17 +526,72 @@ get_usage_static <- function( return(out) } +#' Get usage information for deployed content +#' +#' @description -get_usage <- function(client, from = NULL, to = NULL) { - from <- format(from, "%Y-%m-%dT%H:%M:%SZ") - to <- format(to, "%Y-%m-%dT%H:%M:%SZ") +#' Retrieve content hits for all available content on the server. Available +#' content depends on the user whose API key is in use. Administrator accounts +#' will receive data for all content on the server. Publishers will receive data +#' for all content they own or collaborate on. +#' +#' If no date-times are provided, all usage data will be returned. - usage_raw <- client$inst_content_hits(from, to) +#' @param client A `Connect` R6 client object. +#' @param from Optional `Date` or date-time (`POSIXct` or `POSIXlt`). Only +#' records after this time are returned. If a `Date`, treated as the start of +#' that day in the local time zone; if a date-time, used verbatim. +#' @param to Optional `Date` or date-time (`POSIXct` or `POSIXlt`). Only records +#' before this time are returned. If a `Date`, treated as end of that day +#' (`23:59:59`) in the local time zone; if a date-time, used verbatim. +#' +#' @return A tibble with columns: +#' * `content_guid`: The GUID of the content. +#' * `user_guid`: The GUID of logged-in visitors, NA for anonymous. +#' * `time`: The time of the hit as `POSIXct`. +#' * `path`: The path of the hit. Not recorded for all content types. +#' * `user_agent`: If available, the user agent string for the hit. Not +#' available for all records. +#' +#' @details +#' +#' The data returned by `get_usage()` includes all content types. For Shiny +#' content, the `timestamp` indicates the *start* of the Shiny session. +#' Additional fields for Shiny and non-Shiny are available respectively from +#' `get_usage_shiny()` and `get_usage_static()`. +#' +#' When possible, however, we recommend using `get_usage()` over +#' `get_usage_static()` or `get_usage_shiny()`, as it will be much faster for +#' large datasets. +#' +#' @examples +#' \dontrun{ +#' client <- connect() +#' +#' # Fetch the last 2 days of hits +#' usage <- get_usage(client, from = Sys.Date() - 2, to = Sys.Date()) +#' +#' # Fetch usage after a specified date +#' usage <- get_usage( +#' client, +#' from = as.POSIXct("2025-05-02 12:40:00", tz = "UTC") +#' ) +#' +#' # Fetch all usage +#' usage <- get_usage(client) +#' } +#' +#' @export +get_usage <- function(client, from = NULL, to = NULL) { + usage_raw <- client$inst_content_hits( + from = from, + to = to + ) - parse_connectapi_typed(usage_raw, connectapi_ptypes$usage) + usage <- parse_connectapi_typed(usage_raw, connectapi_ptypes$usage) + fast_unnest_character(usage, "data") } - #' Get Audit Logs from Posit Connect Server #' #' @param src The source object diff --git a/R/parse.R b/R/parse.R index ed9c3d01..f5e53a27 100644 --- a/R/parse.R +++ b/R/parse.R @@ -101,6 +101,63 @@ parse_connectapi <- function(data) { )) } +# Unnests a list column similarly to `tidyr::unnest_wider()`, bringing the +# entries of each list-item up to the top level. Makes some simplifying +# assumptions for the sake of performance: +# 1. All inner variables are treated as character vectors; +# 2. The names of the first entry of the list-column are used as the +# names of variables to extract. +# Performance example: +# > nrow(x_raw) +# [1] 373632 +# > nrow(x_raw) +# [1] 373632 +# > t_tidyr <- system.time( +# + x_tidyr <- tidyr::unnest_wider(x_raw, data) +# + ) +# > t_custom <- system.time( +# + x_custom <- fast_unnest(x_raw, "data") +# + ) +# > identical(x_tidyr, x_custom) +# [1] TRUE +# > t_tidyr +# user system elapsed +# 7.018 0.137 7.172 +# > t_custom +# user system elapsed +# 0.281 0.005 0.285 +fast_unnest_character <- function(df, col_name) { + if (!is.character(col_name)) { + stop("col_name must be a character vector") + } + if (!col_name %in% names(df)) { + stop("col_name is not present in df") + } + + list_col <- df[[col_name]] + + new_cols <- names(list_col[[1]]) + + df2 <- df + for (col in new_cols) { + df2[[col]] <- vapply( + list_col, + function(row) { + if (is.null(row[[col]])) { + NA_character_ + } else { + row[[col]] + } + }, + "1", + USE.NAMES = FALSE + ) + } + + df2[[col_name]] <- NULL + df2 +} + coerce_fsbytes <- function(x, to, ...) { if (is.numeric(x)) { fs::as_fs_bytes(x) diff --git a/man/PositConnect.Rd b/man/PositConnect.Rd index 9d45fbc3..1cc5ccd8 100644 --- a/man/PositConnect.Rd +++ b/man/PositConnect.Rd @@ -117,6 +117,7 @@ Other R6 classes: \item \href{#method-Connect-group_content}{\code{Connect$group_content()}} \item \href{#method-Connect-inst_content_visits}{\code{Connect$inst_content_visits()}} \item \href{#method-Connect-inst_shiny_usage}{\code{Connect$inst_shiny_usage()}} +\item \href{#method-Connect-inst_content_hits}{\code{Connect$inst_content_hits()}} \item \href{#method-Connect-procs}{\code{Connect$procs()}} \item \href{#method-Connect-repo_account}{\code{Connect$repo_account()}} \item \href{#method-Connect-repo_branches}{\code{Connect$repo_branches()}} @@ -1193,6 +1194,27 @@ Get (non-interactive) content visits. } } \if{html}{\out{
}} +\if{html}{\out{}} +\if{latex}{\out{\hypertarget{method-Connect-inst_content_hits}{}}} +\subsection{Method \code{inst_content_hits()}}{ +Get content usage data. +\subsection{Usage}{ +\if{html}{\out{
}}\preformatted{Connect$inst_content_hits(from = NULL, to = NULL)}\if{html}{\out{
}} +} + +\subsection{Arguments}{ +\if{html}{\out{
}} +\describe{ +\item{\code{from}}{Optional \code{Date} or \code{POSIXt}; start of the time window. If a +\code{Date}, coerced to \code{YYYY-MM-DDT00:00:00} in the caller's time zone.} + +\item{\code{to}}{Optional \code{Date} or \code{POSIXt}; end of the time window. If a +\code{Date}, coerced to \code{YYYY-MM-DDT23:59:59} in the caller's time zone.} +} +\if{html}{\out{
}} +} +} +\if{html}{\out{
}} \if{html}{\out{}} \if{latex}{\out{\hypertarget{method-Connect-procs}{}}} \subsection{Method \code{procs()}}{ diff --git a/man/get_usage.Rd b/man/get_usage.Rd new file mode 100644 index 00000000..049e0042 --- /dev/null +++ b/man/get_usage.Rd @@ -0,0 +1,66 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/get.R +\name{get_usage} +\alias{get_usage} +\title{Get usage information for deployed content} +\usage{ +get_usage(client, from = NULL, to = NULL) +} +\arguments{ +\item{client}{A \code{Connect} R6 client object.} + +\item{from}{Optional \code{Date} or date-time (\code{POSIXct} or \code{POSIXlt}). Only +records after this time are returned. If a \code{Date}, treated as the start of +that day in the local time zone; if a date-time, used verbatim.} + +\item{to}{Optional \code{Date} or date-time (\code{POSIXct} or \code{POSIXlt}). Only records +before this time are returned. If a \code{Date}, treated as end of that day +(\code{23:59:59}) in the local time zone; if a date-time, used verbatim.} +} +\value{ +A tibble with columns: +\itemize{ +\item \code{content_guid}: The GUID of the content. +\item \code{user_guid}: The GUID of logged-in visitors, NA for anonymous. +\item \code{time}: The time of the hit as \code{POSIXct}. +\item \code{path}: The path of the hit. Not recorded for all content types. +\item \code{user_agent}: If available, the user agent string for the hit. Not +available for all records. +} +} +\description{ +Retrieve content hits for all available content on the server. Available +content depends on the user whose API key is in use. Administrator accounts +will receive data for all content on the server. Publishers will receive data +for all content they own or collaborate on. + +If no date-times are provided, all usage data will be returned. +} +\details{ +The data returned by \code{get_usage()} includes all content types. For Shiny +content, the \code{timestamp} indicates the \emph{start} of the Shiny session. +Additional fields for Shiny and non-Shiny are available respectively from +\code{get_usage_shiny()} and \code{get_usage_static()}. + +When possible, however, we recommend using \code{get_usage()} over +\code{get_usage_static()} or \code{get_usage_shiny()}, as it will be much faster for +large datasets. +} +\examples{ +\dontrun{ +client <- connect() + +# Fetch the last 2 days of hits +usage <- get_usage(client, from = Sys.Date() - 2, to = Sys.Date()) + +# Fetch usage after a specified date +usage <- get_usage( + client, + from = as.POSIXct("2025-05-02 12:40:00", tz = "UTC") +) + +# Fetch all usage +usage <- get_usage(client) +} + +} diff --git a/tests/testthat/test-get.R b/tests/testthat/test-get.R index 670e8656..9373060b 100644 --- a/tests/testthat/test-get.R +++ b/tests/testthat/test-get.R @@ -330,7 +330,10 @@ test_that("get_packages() works as expected with `content_guid` names in API res test_that("get_content only requests vanity URLs for Connect 2024.06.0 and up", { with_mock_dir("2024.05.0", { - client <- Connect$new(server = "http://connect.example", api_key = "not-a-key") + client <- Connect$new( + server = "http://connect.example", + api_key = "not-a-key" + ) # `$version` is lazy, so we need to call it before `without_internet()`. client$version }) @@ -342,7 +345,10 @@ test_that("get_content only requests vanity URLs for Connect 2024.06.0 and up", }) with_mock_dir("2024.06.0", { - client <- Connect$new(server = "http://connect.example", api_key = "not-a-key") + client <- Connect$new( + server = "http://connect.example", + api_key = "not-a-key" + ) # `$version` is lazy, so we need to call it before `without_internet()`. client$version }) @@ -354,7 +360,10 @@ test_that("get_content only requests vanity URLs for Connect 2024.06.0 and up", }) with_mock_dir("2024.07.0", { - client <- Connect$new(server = "http://connect.example", api_key = "not-a-key") + client <- Connect$new( + server = "http://connect.example", + api_key = "not-a-key" + ) # `$version` is lazy, so we need to call it before `without_internet()`. client$version }) diff --git a/tests/testthat/test-parse.R b/tests/testthat/test-parse.R index 4e182704..9875df88 100644 --- a/tests/testthat/test-parse.R +++ b/tests/testthat/test-parse.R @@ -336,3 +336,49 @@ test_that("works for bad inputs", { expect_s3_class(res$start_time, "POSIXct") expect_s3_class(res$end_time, "POSIXct") }) + +test_that("fast_unnest_character() extracts a list column to character columns", { + df <- tibble::tibble(id = 1:2, animal = c("cat", "dog")) + df$info <- list( + list(path = "/a", user_agent = "ua1"), + list(path = "/b", user_agent = "ua2") + ) + + out <- fast_unnest_character(df, "info") + expect_named(out, c("id", "animal", "path", "user_agent")) + expect_equal(out$id, 1:2) + expect_equal(out$animal, c("cat", "dog")) + expect_equal(out$path, c("/a", "/b")) + expect_equal(out$user_agent, c("ua1", "ua2")) + expect_false("info" %in% names(out)) +}) + +test_that("fast_unnest_character() converts NULL to NA", { + df <- tibble::tibble(id = 1:3, animal = c("cat", "dog", "chinchilla")) + df$info <- list( + list(path = "/a", user_agent = NULL), + list(path = NULL, user_agent = "ua2"), + list(path = NULL, user_agent = NULL) + ) + + out <- fast_unnest_character(df, "info") + expect_equal(out$path, c("/a", NA_character_, NA_character_)) + expect_equal(out$user_agent, c(NA_character_, "ua2", NA_character_)) +}) + +test_that("fast_unnest_character errs when column doesn’t exist", { + df <- data.frame(x = 1:2) + expect_error( + fast_unnest_character(df, "missing_col"), + "col_name is not present in df" + ) +}) + +test_that("fast_unnest_character errs when column isn't a list", { + x <- 1 + df <- data.frame(x = 1:2) + expect_error( + fast_unnest_character(df, x), + "col_name must be a character vector" + ) +}) From 19e45cd4e56d4402de2905d037f2e85302521776 Mon Sep 17 00:00:00 2001 From: Toph Allen Date: Fri, 2 May 2025 16:55:15 -0400 Subject: [PATCH 04/23] changes to allow time zone propagation --- R/parse.R | 4 ++++ R/ptype.R | 2 +- tests/testthat/test-content.R | 2 +- tests/testthat/test-get.R | 2 +- 4 files changed, 7 insertions(+), 3 deletions(-) diff --git a/R/parse.R b/R/parse.R index f5e53a27..145140e8 100644 --- a/R/parse.R +++ b/R/parse.R @@ -58,15 +58,19 @@ ensure_column <- function(data, default, name) { # manual fix because vctrs::vec_cast cannot cast double -> datetime or char -> datetime col <- coerce_datetime(col, default, name = name) } + if (inherits(default, "fs_bytes") && !inherits(col, "fs_bytes")) { col <- coerce_fsbytes(col, default) } + if (inherits(default, "integer64") && !inherits(col, "integer64")) { col <- bit64::as.integer64(col) } + if (inherits(default, "list") && !inherits(col, "list")) { col <- list(col) } + col <- vctrs::vec_cast(col, default, x_arg = name) } data[[name]] <- col diff --git a/R/ptype.R b/R/ptype.R index 4e1b4b5b..7febeba8 100644 --- a/R/ptype.R +++ b/R/ptype.R @@ -1,5 +1,5 @@ NA_datetime_ <- # nolint: object_name_linter - vctrs::new_datetime(NA_real_, tzone = "UTC") + vctrs::new_datetime(NA_real_, tzone = Sys.timezone()) NA_list_ <- # nolint: object_name_linter list(list()) diff --git a/tests/testthat/test-content.R b/tests/testthat/test-content.R index 64202e39..36720700 100644 --- a/tests/testthat/test-content.R +++ b/tests/testthat/test-content.R @@ -397,7 +397,7 @@ test_that("get_log() gets job logs", { source = c("stderr", "stderr", "stderr"), timestamp = structure( c(1733512169.9480169, 1733512169.9480703, 1733512169.9480758), - tzone = "UTC", + tzone = Sys.timezone(), class = c("POSIXct", "POSIXt") ), data = c( diff --git a/tests/testthat/test-get.R b/tests/testthat/test-get.R index 9373060b..efdd6ecf 100644 --- a/tests/testthat/test-get.R +++ b/tests/testthat/test-get.R @@ -202,7 +202,7 @@ test_that("get_vanity_urls() works", { 1602623489, 1677679943 ), - tzone = "UTC", + tzone = Sys.timezone(), class = c("POSIXct", "POSIXt") ) ) From 66e61eff3e32152684e6482a7947eeb2d51383db Mon Sep 17 00:00:00 2001 From: Toph Allen Date: Fri, 2 May 2025 18:13:21 -0400 Subject: [PATCH 05/23] add tests, fix bugs --- R/connect.R | 6 +- .../instrumentation/content/hits-c331ad.json | 52 +++++++++++++ tests/testthat/test-get.R | 77 +++++++++++++++++++ 3 files changed, 132 insertions(+), 3 deletions(-) create mode 100644 tests/testthat/2025.04.0/__api__/v1/instrumentation/content/hits-c331ad.json diff --git a/R/connect.R b/R/connect.R index cf14b194..9d39e22c 100644 --- a/R/connect.R +++ b/R/connect.R @@ -824,16 +824,16 @@ Connect <- R6::R6Class( #' @param to Optional `Date` or `POSIXt`; end of the time window. If a #' `Date`, coerced to `YYYY-MM-DDT23:59:59` in the caller's time zone. inst_content_hits = function(from = NULL, to = NULL) { - error_if_less_than(client$version, "2025.04.0") + error_if_less_than(self$version, "2025.04.0") # If this is called with date objects with no timestamp attached, it's # reasonable to assume that the caller is indicating the days as an # inclusive range. if (inherits(from, "Date")) { - from <- as.POSIXct(paste(from, "00:00:00"), tz = "") + from <- as.POSIXct(paste(from, "00:00:00")) } if (inherits(to, "Date")) { - to <- as.POSIXct(paste(to, "23:59:59"), tz = "") + to <- as.POSIXct(paste(to, "23:59:59")) } self$GET( diff --git a/tests/testthat/2025.04.0/__api__/v1/instrumentation/content/hits-c331ad.json b/tests/testthat/2025.04.0/__api__/v1/instrumentation/content/hits-c331ad.json new file mode 100644 index 00000000..136be68e --- /dev/null +++ b/tests/testthat/2025.04.0/__api__/v1/instrumentation/content/hits-c331ad.json @@ -0,0 +1,52 @@ +[ + { + "id": 8966707, + "user_guid": null, + "content_guid": "475618c9", + "timestamp": "2025-04-30T12:49:16.269904Z", + "data": { + "path": "/hello", + "user_agent": "Datadog/Synthetics" + } + }, + { + "id": 8966708, + "user_guid": null, + "content_guid": "475618c9", + "timestamp": "2025-04-30T12:49:17.002848Z", + "data": { + "path": "/world", + "user_agent": null + } + }, + { + "id": 8967206, + "user_guid": null, + "content_guid": "475618c9", + "timestamp": "2025-04-30T13:01:47.40738Z", + "data": { + "path": "/chinchilla", + "user_agent": "Datadog/Synthetics" + } + }, + { + "id": 8967210, + "user_guid": null, + "content_guid": "475618c9", + "timestamp": "2025-04-30T13:04:13.176791Z", + "data": { + "path": "/lava-lamp", + "user_agent": "Datadog/Synthetics" + } + }, + { + "id": 8966214, + "user_guid": "fecbd383", + "content_guid": "b0eaf295", + "timestamp": "2025-04-30T12:36:13.818466Z", + "data": { + "path": null, + "user_agent": null + } + } +] diff --git a/tests/testthat/test-get.R b/tests/testthat/test-get.R index efdd6ecf..64575a2f 100644 --- a/tests/testthat/test-get.R +++ b/tests/testthat/test-get.R @@ -374,3 +374,80 @@ test_that("get_content only requests vanity URLs for Connect 2024.06.0 and up", ) }) }) + +test_that("get_usage() returns usage data in the expected shape", { + with_mock_dir("2025.04.0", { + client <- connect(server = "https://connect.example", api_key = "fake") + usage <- get_usage( + client, + from = as.POSIXct("2025-04-01 00:00:01", tz = "UTC") + ) + + expect_equal( + usage, + tibble::tibble( + id = c(8966707L, 8966708L, 8967206L, 8967210L, 8966214L), + user_guid = c(NA, NA, NA, NA, "fecbd383"), + content_guid = c( + "475618c9", + "475618c9", + "475618c9", + "475618c9", + "b0eaf295" + ), + timestamp = c( + parse_connect_rfc3339("2025-04-30T12:49:16.269904Z"), + parse_connect_rfc3339("2025-04-30T12:49:17.002848Z"), + parse_connect_rfc3339("2025-04-30T13:01:47.40738Z"), + parse_connect_rfc3339("2025-04-30T13:04:13.176791Z"), + parse_connect_rfc3339("2025-04-30T12:36:13.818466Z") + ), + path = c("/hello", "/world", "/chinchilla", "/lava-lamp", NA), + user_agent = c( + "Datadog/Synthetics", + NA, + "Datadog/Synthetics", + "Datadog/Synthetics", + NA + ) + ) + ) + }) +}) + +test_that("Metrics firehose is called with expected parameters", { + with_mock_api({ + client <- Connect$new(server = "https://connect.example", api_key = "fake") + # $version is loaded lazily, we need it before calling get_usage() + client$version + + without_internet({ + expect_GET( + get_usage(client), + "https://connect.example/__api__/v1/instrumentation/content/hits" + ) + expect_GET( + get_usage( + client, + from = as.POSIXct("2025-04-01 00:00:01", tz = "UTC"), + to = as.POSIXct("2025-04-02 00:00:01", tz = "UTC") + ), + "https://connect.example/__api__/v1/instrumentation/content/hits?from=2025-04-01T00%3A00%3A01Z&to=2025-04-02T00%3A00%3A01Z" + ) + + # Dates are converted to timestamps with the system's time zone, so for + # repeatability we're gonna set it here. + + withr::local_envvar(TZ = "UTC") + + expect_GET( + get_usage( + client, + from = as.Date("2025-04-01"), + to = as.Date("2025-04-02") + ), + "https://connect.example/__api__/v1/instrumentation/content/hits?from=2025-04-01T00%3A00%3A00Z&to=2025-04-02T23%3A59%3A59Z" + ) + }) + }) +}) From 24f9dc32bf9b0f3e0dd189f62f8256f2f1a0c58a Mon Sep 17 00:00:00 2001 From: Toph Allen Date: Fri, 2 May 2025 18:42:06 -0400 Subject: [PATCH 06/23] fix doc problem --- R/get.R | 5 +++-- R/parse.R | 2 +- man/get_usage.Rd | 5 +++-- 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/R/get.R b/R/get.R index 1ba4051f..c7737768 100644 --- a/R/get.R +++ b/R/get.R @@ -546,9 +546,10 @@ get_usage_static <- function( #' (`23:59:59`) in the local time zone; if a date-time, used verbatim. #' #' @return A tibble with columns: -#' * `content_guid`: The GUID of the content. +#' * `id`: An identifier for the record. #' * `user_guid`: The GUID of logged-in visitors, NA for anonymous. -#' * `time`: The time of the hit as `POSIXct`. +#' * `content_guid`: The GUID of the content. +#' * `timestamp`: The time of the hit as `POSIXct`. #' * `path`: The path of the hit. Not recorded for all content types. #' * `user_agent`: If available, the user agent string for the hit. Not #' available for all records. diff --git a/R/parse.R b/R/parse.R index 145140e8..2a2ebeb0 100644 --- a/R/parse.R +++ b/R/parse.R @@ -120,7 +120,7 @@ parse_connectapi <- function(data) { # + x_tidyr <- tidyr::unnest_wider(x_raw, data) # + ) # > t_custom <- system.time( -# + x_custom <- fast_unnest(x_raw, "data") +# + x_custom <- fast_unnest_character(x_raw, "data") # + ) # > identical(x_tidyr, x_custom) # [1] TRUE diff --git a/man/get_usage.Rd b/man/get_usage.Rd index 049e0042..de423eda 100644 --- a/man/get_usage.Rd +++ b/man/get_usage.Rd @@ -20,9 +20,10 @@ before this time are returned. If a \code{Date}, treated as end of that day \value{ A tibble with columns: \itemize{ -\item \code{content_guid}: The GUID of the content. +\item \code{id}: An identifier for the record. \item \code{user_guid}: The GUID of logged-in visitors, NA for anonymous. -\item \code{time}: The time of the hit as \code{POSIXct}. +\item \code{content_guid}: The GUID of the content. +\item \code{timestamp}: The time of the hit as \code{POSIXct}. \item \code{path}: The path of the hit. Not recorded for all content types. \item \code{user_agent}: If available, the user agent string for the hit. Not available for all records. From 6442b4d58abb807e658f6248f0cd877581e7571d Mon Sep 17 00:00:00 2001 From: Toph Allen Date: Fri, 2 May 2025 19:14:20 -0400 Subject: [PATCH 07/23] make lintr happier --- R/parse.R | 2 ++ tests/testthat/test-get.R | 10 ++++++++-- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/R/parse.R b/R/parse.R index 2a2ebeb0..f01de490 100644 --- a/R/parse.R +++ b/R/parse.R @@ -105,6 +105,7 @@ parse_connectapi <- function(data) { )) } +# nolint start # Unnests a list column similarly to `tidyr::unnest_wider()`, bringing the # entries of each list-item up to the top level. Makes some simplifying # assumptions for the sake of performance: @@ -130,6 +131,7 @@ parse_connectapi <- function(data) { # > t_custom # user system elapsed # 0.281 0.005 0.285 +# nolint end fast_unnest_character <- function(df, col_name) { if (!is.character(col_name)) { stop("col_name must be a character vector") diff --git a/tests/testthat/test-get.R b/tests/testthat/test-get.R index 64575a2f..d714f769 100644 --- a/tests/testthat/test-get.R +++ b/tests/testthat/test-get.R @@ -432,7 +432,10 @@ test_that("Metrics firehose is called with expected parameters", { from = as.POSIXct("2025-04-01 00:00:01", tz = "UTC"), to = as.POSIXct("2025-04-02 00:00:01", tz = "UTC") ), - "https://connect.example/__api__/v1/instrumentation/content/hits?from=2025-04-01T00%3A00%3A01Z&to=2025-04-02T00%3A00%3A01Z" + paste0( + "https://connect.example/__api__/v1/instrumentation/content/hits?", + "from=2025-04-01T00%3A00%3A01Z&to=2025-04-02T00%3A00%3A01Z" + ) ) # Dates are converted to timestamps with the system's time zone, so for @@ -446,7 +449,10 @@ test_that("Metrics firehose is called with expected parameters", { from = as.Date("2025-04-01"), to = as.Date("2025-04-02") ), - "https://connect.example/__api__/v1/instrumentation/content/hits?from=2025-04-01T00%3A00%3A00Z&to=2025-04-02T23%3A59%3A59Z" + paste0( + "https://connect.example/__api__/v1/instrumentation/content/hits?", + "from=2025-04-01T00%3A00%3A00Z&to=2025-04-02T23%3A59%3A59Z" + ) ) }) }) From 919bb0a7e4bd8c1a31484fdbdc7793f338d1919f Mon Sep 17 00:00:00 2001 From: Toph Allen Date: Fri, 2 May 2025 19:23:27 -0400 Subject: [PATCH 08/23] remove cyclocomp linter --- .lintr | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.lintr b/.lintr index 226d5c03..41b22f67 100644 --- a/.lintr +++ b/.lintr @@ -1,7 +1,7 @@ linters: linters_with_defaults( line_length_linter = line_length_linter(120L), object_name_linter = object_name_linter(styles = c("snake_case", "symbols", "CamelCase")), - cyclocomp_linter = cyclocomp_linter(30L), + cyclocomp_linter = NULL, # Issues with R6 classes. object_length_linter(32L), indentation_linter = indentation_linter(hanging_indent_style = "tidy"), return_linter = NULL From 7b05c73c79dbf51d332f0246d0cba60a7520cfc7 Mon Sep 17 00:00:00 2001 From: Toph Allen Date: Tue, 15 Jul 2025 17:49:25 -0400 Subject: [PATCH 09/23] respond to comments --- R/connect.R | 31 +++---------------------------- R/get.R | 28 ++++++++++++++++------------ tests/testthat/test-get.R | 29 +++++++---------------------- 3 files changed, 26 insertions(+), 62 deletions(-) diff --git a/R/connect.R b/R/connect.R index 9d39e22c..905fd07e 100644 --- a/R/connect.R +++ b/R/connect.R @@ -818,33 +818,6 @@ Connect <- R6::R6Class( self$GET(path, query = query) }, - #' @description Get content usage data. - #' @param from Optional `Date` or `POSIXt`; start of the time window. If a - #' `Date`, coerced to `YYYY-MM-DDT00:00:00` in the caller's time zone. - #' @param to Optional `Date` or `POSIXt`; end of the time window. If a - #' `Date`, coerced to `YYYY-MM-DDT23:59:59` in the caller's time zone. - inst_content_hits = function(from = NULL, to = NULL) { - error_if_less_than(self$version, "2025.04.0") - - # If this is called with date objects with no timestamp attached, it's - # reasonable to assume that the caller is indicating the days as an - # inclusive range. - if (inherits(from, "Date")) { - from <- as.POSIXct(paste(from, "00:00:00")) - } - if (inherits(to, "Date")) { - to <- as.POSIXct(paste(to, "23:59:59")) - } - - self$GET( - v1_url("instrumentation", "content", "hits"), - query = list( - from = make_timestamp(from), - to = make_timestamp(to) - ) - ) - }, - #' @description Get running processes. procs = function() { warn_experimental("procs") @@ -932,7 +905,9 @@ Connect <- R6::R6Class( docs = function(docs = "api", browse = TRUE) { stopifnot(docs %in% c("admin", "user", "api")) url <- paste0(self$server, "/__docs__/", docs) - if (browse) utils::browseURL(url) + if (browse) { + utils::browseURL(url) + } url }, diff --git a/R/get.R b/R/get.R index c7737768..24c9a3ed 100644 --- a/R/get.R +++ b/R/get.R @@ -538,12 +538,12 @@ get_usage_static <- function( #' If no date-times are provided, all usage data will be returned. #' @param client A `Connect` R6 client object. -#' @param from Optional `Date` or date-time (`POSIXct` or `POSIXlt`). Only -#' records after this time are returned. If a `Date`, treated as the start of -#' that day in the local time zone; if a date-time, used verbatim. -#' @param to Optional `Date` or date-time (`POSIXct` or `POSIXlt`). Only records -#' before this time are returned. If a `Date`, treated as end of that day -#' (`23:59:59`) in the local time zone; if a date-time, used verbatim. +#' @param from Optional date-time (`POSIXct` or `POSIXlt`). Only +#' records after this time are returned. If not provided, records +#' are returned back to the first record available. +#' @param to Optional date-time (`POSIXct` or `POSIXlt`). Only records +#' before this time are returned. If not provided, all records up to +#' the most recent are returned. #' #' @return A tibble with columns: #' * `id`: An identifier for the record. @@ -562,8 +562,8 @@ get_usage_static <- function( #' `get_usage_shiny()` and `get_usage_static()`. #' #' When possible, however, we recommend using `get_usage()` over -#' `get_usage_static()` or `get_usage_shiny()`, as it will be much faster for -#' large datasets. +#' `get_usage_static()` or `get_usage_shiny()`, as it will perform better +#' than those endpoints, which use pagination. #' #' @examples #' \dontrun{ @@ -584,11 +584,15 @@ get_usage_static <- function( #' #' @export get_usage <- function(client, from = NULL, to = NULL) { - usage_raw <- client$inst_content_hits( - from = from, - to = to - ) + error_if_less_than(client$version, "2025.04.0") + usage_raw <- client$GET( + v1_url("instrumentation", "content", "hits"), + query = list( + from = make_timestamp(from), + to = make_timestamp(to) + ) + ) usage <- parse_connectapi_typed(usage_raw, connectapi_ptypes$usage) fast_unnest_character(usage, "data") } diff --git a/tests/testthat/test-get.R b/tests/testthat/test-get.R index d714f769..1807d88d 100644 --- a/tests/testthat/test-get.R +++ b/tests/testthat/test-get.R @@ -396,11 +396,13 @@ test_that("get_usage() returns usage data in the expected shape", { "b0eaf295" ), timestamp = c( - parse_connect_rfc3339("2025-04-30T12:49:16.269904Z"), - parse_connect_rfc3339("2025-04-30T12:49:17.002848Z"), - parse_connect_rfc3339("2025-04-30T13:01:47.40738Z"), - parse_connect_rfc3339("2025-04-30T13:04:13.176791Z"), - parse_connect_rfc3339("2025-04-30T12:36:13.818466Z") + parse_connect_rfc3339(c( + "2025-04-30T12:49:16.269904Z", + "2025-04-30T12:49:17.002848Z", + "2025-04-30T13:01:47.40738Z", + "2025-04-30T13:04:13.176791Z", + "2025-04-30T12:36:13.818466Z" + )) ), path = c("/hello", "/world", "/chinchilla", "/lava-lamp", NA), user_agent = c( @@ -437,23 +439,6 @@ test_that("Metrics firehose is called with expected parameters", { "from=2025-04-01T00%3A00%3A01Z&to=2025-04-02T00%3A00%3A01Z" ) ) - - # Dates are converted to timestamps with the system's time zone, so for - # repeatability we're gonna set it here. - - withr::local_envvar(TZ = "UTC") - - expect_GET( - get_usage( - client, - from = as.Date("2025-04-01"), - to = as.Date("2025-04-02") - ), - paste0( - "https://connect.example/__api__/v1/instrumentation/content/hits?", - "from=2025-04-01T00%3A00%3A00Z&to=2025-04-02T23%3A59%3A59Z" - ) - ) }) }) }) From 01a6ccd2e9f5ee2dab1a1ebd276491e540e5eaf1 Mon Sep 17 00:00:00 2001 From: Toph Allen Date: Tue, 15 Jul 2025 17:55:12 -0400 Subject: [PATCH 10/23] edit comment --- R/parse.R | 22 +--------------------- 1 file changed, 1 insertion(+), 21 deletions(-) diff --git a/R/parse.R b/R/parse.R index f01de490..dd52ba13 100644 --- a/R/parse.R +++ b/R/parse.R @@ -108,30 +108,10 @@ parse_connectapi <- function(data) { # nolint start # Unnests a list column similarly to `tidyr::unnest_wider()`, bringing the # entries of each list-item up to the top level. Makes some simplifying -# assumptions for the sake of performance: +# assumptions. # 1. All inner variables are treated as character vectors; # 2. The names of the first entry of the list-column are used as the # names of variables to extract. -# Performance example: -# > nrow(x_raw) -# [1] 373632 -# > nrow(x_raw) -# [1] 373632 -# > t_tidyr <- system.time( -# + x_tidyr <- tidyr::unnest_wider(x_raw, data) -# + ) -# > t_custom <- system.time( -# + x_custom <- fast_unnest_character(x_raw, "data") -# + ) -# > identical(x_tidyr, x_custom) -# [1] TRUE -# > t_tidyr -# user system elapsed -# 7.018 0.137 7.172 -# > t_custom -# user system elapsed -# 0.281 0.005 0.285 -# nolint end fast_unnest_character <- function(df, col_name) { if (!is.character(col_name)) { stop("col_name must be a character vector") From 567fea312888f93136728b0f76b78f88a8afb06f Mon Sep 17 00:00:00 2001 From: Toph Allen Date: Thu, 17 Jul 2025 18:01:47 -0400 Subject: [PATCH 11/23] Apply suggestion from @nealrichardson Co-authored-by: Neal Richardson --- R/get.R | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/R/get.R b/R/get.R index 24c9a3ed..8698db22 100644 --- a/R/get.R +++ b/R/get.R @@ -559,7 +559,7 @@ get_usage_static <- function( #' The data returned by `get_usage()` includes all content types. For Shiny #' content, the `timestamp` indicates the *start* of the Shiny session. #' Additional fields for Shiny and non-Shiny are available respectively from -#' `get_usage_shiny()` and `get_usage_static()`. +#' [get_usage_shiny()] and [get_usage_static()]. #' #' When possible, however, we recommend using `get_usage()` over #' `get_usage_static()` or `get_usage_shiny()`, as it will perform better From f68f753171931a6cae1f06a2ff48f25b8e5f9513 Mon Sep 17 00:00:00 2001 From: Toph Allen Date: Thu, 17 Jul 2025 18:01:58 -0400 Subject: [PATCH 12/23] Apply suggestion from @nealrichardson Co-authored-by: Neal Richardson --- R/parse.R | 1 - 1 file changed, 1 deletion(-) diff --git a/R/parse.R b/R/parse.R index dd52ba13..a4399117 100644 --- a/R/parse.R +++ b/R/parse.R @@ -105,7 +105,6 @@ parse_connectapi <- function(data) { )) } -# nolint start # Unnests a list column similarly to `tidyr::unnest_wider()`, bringing the # entries of each list-item up to the top level. Makes some simplifying # assumptions. From 26980e369098129ce60f87786f4db804176f605d Mon Sep 17 00:00:00 2001 From: Toph Allen Date: Thu, 17 Jul 2025 18:02:05 -0400 Subject: [PATCH 13/23] Apply suggestion from @nealrichardson Co-authored-by: Neal Richardson --- R/parse.R | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/R/parse.R b/R/parse.R index a4399117..dfb688af 100644 --- a/R/parse.R +++ b/R/parse.R @@ -134,7 +134,7 @@ fast_unnest_character <- function(df, col_name) { row[[col]] } }, - "1", + character(1), USE.NAMES = FALSE ) } From 491f7df9ef4a2b475b62541fc0602e64a980f410 Mon Sep 17 00:00:00 2001 From: Toph Allen Date: Thu, 17 Jul 2025 18:02:24 -0400 Subject: [PATCH 14/23] Apply suggestion from @nealrichardson Co-authored-by: Neal Richardson --- R/get.R | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/R/get.R b/R/get.R index 8698db22..ac9f6c59 100644 --- a/R/get.R +++ b/R/get.R @@ -562,8 +562,7 @@ get_usage_static <- function( #' [get_usage_shiny()] and [get_usage_static()]. #' #' When possible, however, we recommend using `get_usage()` over -#' `get_usage_static()` or `get_usage_shiny()`, as it will perform better -#' than those endpoints, which use pagination. +#' `get_usage_static()` or `get_usage_shiny()`, as it is faster and more efficient. #' #' @examples #' \dontrun{ From b474ed9b88a0fbff3854ea8d0e93b7213d730bb4 Mon Sep 17 00:00:00 2001 From: Toph Allen Date: Thu, 17 Jul 2025 19:14:09 -0400 Subject: [PATCH 15/23] update documentation --- R/get.R | 4 +++- man/PositConnect.Rd | 22 ---------------------- man/get_usage.Rd | 19 ++++++++++--------- 3 files changed, 13 insertions(+), 32 deletions(-) diff --git a/R/get.R b/R/get.R index ac9f6c59..23ead6e5 100644 --- a/R/get.R +++ b/R/get.R @@ -559,7 +559,9 @@ get_usage_static <- function( #' The data returned by `get_usage()` includes all content types. For Shiny #' content, the `timestamp` indicates the *start* of the Shiny session. #' Additional fields for Shiny and non-Shiny are available respectively from -#' [get_usage_shiny()] and [get_usage_static()]. +#' [get_usage_shiny()] and [get_usage_static()]. `get_usage_shiny()` includes a +#' field for the session end time; `get_usage_static()` includes variant, +#' rendering, and bundle identifiers for the visited content. #' #' When possible, however, we recommend using `get_usage()` over #' `get_usage_static()` or `get_usage_shiny()`, as it is faster and more efficient. diff --git a/man/PositConnect.Rd b/man/PositConnect.Rd index 1cc5ccd8..9d45fbc3 100644 --- a/man/PositConnect.Rd +++ b/man/PositConnect.Rd @@ -117,7 +117,6 @@ Other R6 classes: \item \href{#method-Connect-group_content}{\code{Connect$group_content()}} \item \href{#method-Connect-inst_content_visits}{\code{Connect$inst_content_visits()}} \item \href{#method-Connect-inst_shiny_usage}{\code{Connect$inst_shiny_usage()}} -\item \href{#method-Connect-inst_content_hits}{\code{Connect$inst_content_hits()}} \item \href{#method-Connect-procs}{\code{Connect$procs()}} \item \href{#method-Connect-repo_account}{\code{Connect$repo_account()}} \item \href{#method-Connect-repo_branches}{\code{Connect$repo_branches()}} @@ -1194,27 +1193,6 @@ Get (non-interactive) content visits. } } \if{html}{\out{
}} -\if{html}{\out{}} -\if{latex}{\out{\hypertarget{method-Connect-inst_content_hits}{}}} -\subsection{Method \code{inst_content_hits()}}{ -Get content usage data. -\subsection{Usage}{ -\if{html}{\out{
}}\preformatted{Connect$inst_content_hits(from = NULL, to = NULL)}\if{html}{\out{
}} -} - -\subsection{Arguments}{ -\if{html}{\out{
}} -\describe{ -\item{\code{from}}{Optional \code{Date} or \code{POSIXt}; start of the time window. If a -\code{Date}, coerced to \code{YYYY-MM-DDT00:00:00} in the caller's time zone.} - -\item{\code{to}}{Optional \code{Date} or \code{POSIXt}; end of the time window. If a -\code{Date}, coerced to \code{YYYY-MM-DDT23:59:59} in the caller's time zone.} -} -\if{html}{\out{
}} -} -} -\if{html}{\out{
}} \if{html}{\out{}} \if{latex}{\out{\hypertarget{method-Connect-procs}{}}} \subsection{Method \code{procs()}}{ diff --git a/man/get_usage.Rd b/man/get_usage.Rd index de423eda..ff8732c5 100644 --- a/man/get_usage.Rd +++ b/man/get_usage.Rd @@ -9,13 +9,13 @@ get_usage(client, from = NULL, to = NULL) \arguments{ \item{client}{A \code{Connect} R6 client object.} -\item{from}{Optional \code{Date} or date-time (\code{POSIXct} or \code{POSIXlt}). Only -records after this time are returned. If a \code{Date}, treated as the start of -that day in the local time zone; if a date-time, used verbatim.} +\item{from}{Optional date-time (\code{POSIXct} or \code{POSIXlt}). Only +records after this time are returned. If not provided, records +are returned back to the first record available.} -\item{to}{Optional \code{Date} or date-time (\code{POSIXct} or \code{POSIXlt}). Only records -before this time are returned. If a \code{Date}, treated as end of that day -(\code{23:59:59}) in the local time zone; if a date-time, used verbatim.} +\item{to}{Optional date-time (\code{POSIXct} or \code{POSIXlt}). Only records +before this time are returned. If not provided, all records up to +the most recent are returned.} } \value{ A tibble with columns: @@ -41,11 +41,12 @@ If no date-times are provided, all usage data will be returned. The data returned by \code{get_usage()} includes all content types. For Shiny content, the \code{timestamp} indicates the \emph{start} of the Shiny session. Additional fields for Shiny and non-Shiny are available respectively from -\code{get_usage_shiny()} and \code{get_usage_static()}. +\code{\link[=get_usage_shiny]{get_usage_shiny()}} and \code{\link[=get_usage_static]{get_usage_static()}}. \code{get_usage_shiny()} includes a +field for the session end time; \code{get_usage_static()} includes variant, +rendering, and bundle identifiers for the visited content. When possible, however, we recommend using \code{get_usage()} over -\code{get_usage_static()} or \code{get_usage_shiny()}, as it will be much faster for -large datasets. +\code{get_usage_static()} or \code{get_usage_shiny()}, as it is faster and more efficient. } \examples{ \dontrun{ From 30ea3334b49ead4b344198f5ba44c3bcf24c5aaa Mon Sep 17 00:00:00 2001 From: Toph Allen Date: Fri, 18 Jul 2025 15:10:48 -0400 Subject: [PATCH 16/23] Do not parse usage immediately; provide as.data.frame method --- DESCRIPTION | 1 + NAMESPACE | 3 + R/get.R | 96 ++++++++++++++++++++++---- R/parse.R | 37 ---------- man/as.data.frame.connect_list_hits.Rd | 24 +++++++ man/as_tibble.connect_list_hits.Rd | 20 ++++++ man/get_usage.Rd | 43 +++++++++--- tests/testthat/test-get.R | 26 ++++++- 8 files changed, 189 insertions(+), 61 deletions(-) create mode 100644 man/as.data.frame.connect_list_hits.Rd create mode 100644 man/as_tibble.connect_list_hits.Rd diff --git a/DESCRIPTION b/DESCRIPTION index a3906bb4..ba602bee 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -57,6 +57,7 @@ Suggests: rsconnect, spelling, testthat, + tidyr, webshot2, withr VignetteBuilder: diff --git a/NAMESPACE b/NAMESPACE index b58dd82c..30118f21 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -5,7 +5,9 @@ S3method("[",connect_tag_tree) S3method("[[",connect_tag_tree) S3method(api_build,op_base_connect) S3method(api_build,op_head) +S3method(as.data.frame,connect_list_hits) S3method(as.data.frame,tbl_connect) +S3method(as_tibble,connect_list_hits) S3method(connect_vars,op_base) S3method(connect_vars,op_single) S3method(connect_vars,tbl_connect) @@ -160,6 +162,7 @@ importFrom(rlang,"%||%") importFrom(rlang,":=") importFrom(rlang,arg_match) importFrom(rlang,is_string) +importFrom(tibble,as_tibble) importFrom(utils,browseURL) importFrom(utils,capture.output) importFrom(utils,compareVersion) diff --git a/R/get.R b/R/get.R index 3a98d617..0f078c54 100644 --- a/R/get.R +++ b/R/get.R @@ -529,14 +529,13 @@ get_usage_static <- function( #' Get usage information for deployed content #' #' @description - #' Retrieve content hits for all available content on the server. Available #' content depends on the user whose API key is in use. Administrator accounts #' will receive data for all content on the server. Publishers will receive data #' for all content they own or collaborate on. #' #' If no date-times are provided, all usage data will be returned. - +#' #' @param client A `Connect` R6 client object. #' @param from Optional date-time (`POSIXct` or `POSIXlt`). Only #' records after this time are returned. If not provided, records @@ -545,14 +544,28 @@ get_usage_static <- function( #' before this time are returned. If not provided, all records up to #' the most recent are returned. #' -#' @return A tibble with columns: -#' * `id`: An identifier for the record. -#' * `user_guid`: The GUID of logged-in visitors, NA for anonymous. -#' * `content_guid`: The GUID of the content. -#' * `timestamp`: The time of the hit as `POSIXct`. -#' * `path`: The path of the hit. Not recorded for all content types. -#' * `user_agent`: If available, the user agent string for the hit. Not -#' available for all records. +#' @return A list of usage records. Each record is a list with all elements +#' as character strings unless otherwise specified. +#' +#' * `id`: An integer identifier for the hit. +#' * `user_guid`: The user GUID if the visitor is logged-in, `NULL` for +#' anonymous hits. +#' * `content_guid`: The GUID of the visited content. +#' * `timestamp`: The time of the hit in RFC3339 format. +#' * `data`: A nested list with optional fields: +#' * `path`: The request path (if recorded). +#' * `user_agent`: The user agent string (if available). +#' +#' Use [as.data.frame()] or [tibble::as_tibble()] to convert to a flat +#' table with parsed types. In the resulting data frame: +#' +#' * `timestamp` is parsed to `POSIXct`. +#' * `path` and `user_agent` are extracted from the nested `data` field. +#' +#' By default, [as.data.frame()] attempts to extract the nested fields using +#' the \pkg{tidyr} package. If \pkg{tidyr} is not available, or if you want to +#' skip unnesting, call `as.data.frame(x, unnest = FALSE)` to leave `data` as +#' a list-column. #' #' @details #' @@ -566,6 +579,8 @@ get_usage_static <- function( #' When possible, however, we recommend using `get_usage()` over #' `get_usage_static()` or `get_usage_shiny()`, as it is faster and more efficient. #' +#' @seealso [as.data.frame.connect_list_hits()], [as_tibble.connect_list_hits()] +#' #' @examples #' \dontrun{ #' client <- connect() @@ -573,7 +588,7 @@ get_usage_static <- function( #' # Fetch the last 2 days of hits #' usage <- get_usage(client, from = Sys.Date() - 2, to = Sys.Date()) #' -#' # Fetch usage after a specified date +#' # Fetch usage after a specified date and convert to a data frame. #' usage <- get_usage( #' client, #' from = as.POSIXct("2025-05-02 12:40:00", tz = "UTC") @@ -581,21 +596,74 @@ get_usage_static <- function( #' #' # Fetch all usage #' usage <- get_usage(client) +#' +#' # Convert to tibble or data frame +#' usage_df <- tibble::as_tibble(usage) +#' +#' # Skip unnesting if tidyr is not installed +#' usage_df <- as.data.frame(usage, unnest = FALSE) #' } #' #' @export get_usage <- function(client, from = NULL, to = NULL) { error_if_less_than(client$version, "2025.04.0") - usage_raw <- client$GET( + usage <- client$GET( v1_url("instrumentation", "content", "hits"), query = list( from = make_timestamp(from), to = make_timestamp(to) ) ) - usage <- parse_connectapi_typed(usage_raw, connectapi_ptypes$usage) - fast_unnest_character(usage, "data") + + class(usage) <- c("connect_list_hits", class(usage)) + usage +} + +#' Convert usage data to a data frame +#' +#' @description +#' Converts an object returned by [get_usage()] into a data frame with parsed +#' column types. By default, extracts `path` and `user_agent` from the `data` +#' field, if available. +#' +#' @param x A `connect_list_hits` object (from [get_usage()]). +#' @param unnest Logical; if `TRUE` (default), extracts nested fields using +#' \pkg{tidyr}. Set to `FALSE` to skip unnesting. +#' @param ... Ignored. +#' +#' @return A `data.frame` with one row per usage record. +#' @export +#' @method as.data.frame connect_list_hits +as.data.frame.connect_list_hits <- function(x, unnest = TRUE, ...) { + usage_df <- parse_connectapi_typed(x, connectapi_ptypes$usage) + if (unnest) { + if (!requireNamespace("tidyr", quietly = TRUE)) { + stop( + "`unnest = TRUE` requires tidyr. Install tidyr or set `unnest = FALSE`.", + call. = FALSE + ) + } + usage_df <- tidyr::unnest_wider(usage_df, data, ptype = list(path = character(0), user_agent = character(0))) + } + as.data.frame(usage_df) +} + +#' Convert usage data to a tibble +#' +#' @description +#' Converts an object returned by [get_usage()] to a tibble via +#' [as.data.frame.connect_list_hits()]. +#' +#' @param x A `connect_list_hits` object. +#' @param ... Passed to [as.data.frame()]. +#' +#' @return A tibble with one row per usage record. +#' @export +#' @importFrom tibble as_tibble +#' @method as_tibble connect_list_hits +as_tibble.connect_list_hits <- function(x, ...) { + tibble::as_tibble(as.data.frame(x, ...)) } #' Get Audit Logs from Posit Connect Server diff --git a/R/parse.R b/R/parse.R index dfb688af..22d582fe 100644 --- a/R/parse.R +++ b/R/parse.R @@ -105,43 +105,6 @@ parse_connectapi <- function(data) { )) } -# Unnests a list column similarly to `tidyr::unnest_wider()`, bringing the -# entries of each list-item up to the top level. Makes some simplifying -# assumptions. -# 1. All inner variables are treated as character vectors; -# 2. The names of the first entry of the list-column are used as the -# names of variables to extract. -fast_unnest_character <- function(df, col_name) { - if (!is.character(col_name)) { - stop("col_name must be a character vector") - } - if (!col_name %in% names(df)) { - stop("col_name is not present in df") - } - - list_col <- df[[col_name]] - - new_cols <- names(list_col[[1]]) - - df2 <- df - for (col in new_cols) { - df2[[col]] <- vapply( - list_col, - function(row) { - if (is.null(row[[col]])) { - NA_character_ - } else { - row[[col]] - } - }, - character(1), - USE.NAMES = FALSE - ) - } - - df2[[col_name]] <- NULL - df2 -} coerce_fsbytes <- function(x, to, ...) { if (is.numeric(x)) { diff --git a/man/as.data.frame.connect_list_hits.Rd b/man/as.data.frame.connect_list_hits.Rd new file mode 100644 index 00000000..eea7d41d --- /dev/null +++ b/man/as.data.frame.connect_list_hits.Rd @@ -0,0 +1,24 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/get.R +\name{as.data.frame.connect_list_hits} +\alias{as.data.frame.connect_list_hits} +\title{Convert usage data to a data frame} +\usage{ +\method{as.data.frame}{connect_list_hits}(x, unnest = TRUE, ...) +} +\arguments{ +\item{x}{A \code{connect_list_hits} object (from \code{\link[=get_usage]{get_usage()}}).} + +\item{unnest}{Logical; if \code{TRUE} (default), extracts nested fields using +\pkg{tidyr}. Set to \code{FALSE} to skip unnesting.} + +\item{...}{Ignored.} +} +\value{ +A \code{data.frame} with one row per usage record. +} +\description{ +Converts an object returned by \code{\link[=get_usage]{get_usage()}} into a data frame with parsed +column types. By default, extracts \code{path} and \code{user_agent} from the \code{data} +field, if available. +} diff --git a/man/as_tibble.connect_list_hits.Rd b/man/as_tibble.connect_list_hits.Rd new file mode 100644 index 00000000..7f3221dc --- /dev/null +++ b/man/as_tibble.connect_list_hits.Rd @@ -0,0 +1,20 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/get.R +\name{as_tibble.connect_list_hits} +\alias{as_tibble.connect_list_hits} +\title{Convert usage data to a tibble} +\usage{ +\method{as_tibble}{connect_list_hits}(x, ...) +} +\arguments{ +\item{x}{A \code{connect_list_hits} object.} + +\item{...}{Passed to \code{\link[=as.data.frame]{as.data.frame()}}.} +} +\value{ +A tibble with one row per usage record. +} +\description{ +Converts an object returned by \code{\link[=get_usage]{get_usage()}} to a tibble via +\code{\link[=as.data.frame.connect_list_hits]{as.data.frame.connect_list_hits()}}. +} diff --git a/man/get_usage.Rd b/man/get_usage.Rd index ff8732c5..40372129 100644 --- a/man/get_usage.Rd +++ b/man/get_usage.Rd @@ -18,17 +18,33 @@ before this time are returned. If not provided, all records up to the most recent are returned.} } \value{ -A tibble with columns: +A list of usage records. Each record is a list with all elements +as character strings unless otherwise specified. \itemize{ -\item \code{id}: An identifier for the record. -\item \code{user_guid}: The GUID of logged-in visitors, NA for anonymous. -\item \code{content_guid}: The GUID of the content. -\item \code{timestamp}: The time of the hit as \code{POSIXct}. -\item \code{path}: The path of the hit. Not recorded for all content types. -\item \code{user_agent}: If available, the user agent string for the hit. Not -available for all records. +\item \code{id}: An integer identifier for the hit. +\item \code{user_guid}: The user GUID if the visitor is logged-in, \code{NULL} for +anonymous hits. +\item \code{content_guid}: The GUID of the visited content. +\item \code{timestamp}: The time of the hit in RFC3339 format. +\item \code{data}: A nested list with optional fields: +\itemize{ +\item \code{path}: The request path (if recorded). +\item \code{user_agent}: The user agent string (if available). } } + +Use \code{\link[=as.data.frame]{as.data.frame()}} or \code{\link[tibble:as_tibble]{tibble::as_tibble()}} to convert to a flat +table with parsed types. In the resulting data frame: +\itemize{ +\item \code{timestamp} is parsed to \code{POSIXct}. +\item \code{path} and \code{user_agent} are extracted from the nested \code{data} field. +} + +By default, \code{\link[=as.data.frame]{as.data.frame()}} attempts to extract the nested fields using +the \pkg{tidyr} package. If \pkg{tidyr} is not available, or if you want to +skip unnesting, call \code{as.data.frame(x, unnest = FALSE)} to leave \code{data} as +a list-column. +} \description{ Retrieve content hits for all available content on the server. Available content depends on the user whose API key is in use. Administrator accounts @@ -55,7 +71,7 @@ client <- connect() # Fetch the last 2 days of hits usage <- get_usage(client, from = Sys.Date() - 2, to = Sys.Date()) -# Fetch usage after a specified date +# Fetch usage after a specified date and convert to a data frame. usage <- get_usage( client, from = as.POSIXct("2025-05-02 12:40:00", tz = "UTC") @@ -63,6 +79,15 @@ usage <- get_usage( # Fetch all usage usage <- get_usage(client) + +# Convert to tibble or data frame +usage_df <- tibble::as_tibble(usage) + +# Skip unnesting if tidyr is not installed +usage_df <- as.data.frame(usage, unnest = FALSE) } } +\seealso{ +\code{\link[=as.data.frame.connect_list_hits]{as.data.frame.connect_list_hits()}}, \code{\link[=as_tibble.connect_list_hits]{as_tibble.connect_list_hits()}} +} diff --git a/tests/testthat/test-get.R b/tests/testthat/test-get.R index 1807d88d..c2968659 100644 --- a/tests/testthat/test-get.R +++ b/tests/testthat/test-get.R @@ -383,8 +383,28 @@ test_that("get_usage() returns usage data in the expected shape", { from = as.POSIXct("2025-04-01 00:00:01", tz = "UTC") ) + expect_s3_class(usage, "connect_list_hits") + expect_s3_class(usage, "list") + + expect_length(usage, 5) + + # Check first element expect_equal( - usage, + usage[[1]], + list( + id = 8966707L, + user_guid = NA_character_, + content_guid = "475618c9", + timestamp = "2025-04-30T12:49:16.269904Z", + path = "/hello", + user_agent = "Datadog/Synthetics" + ) + ) + + # Check conversion to data.frame + usage_df <- as.data.frame(usage) + expect_equal( + usage_df, tibble::tibble( id = c(8966707L, 8966708L, 8967206L, 8967210L, 8966214L), user_guid = c(NA, NA, NA, NA, "fecbd383"), @@ -414,6 +434,10 @@ test_that("get_usage() returns usage data in the expected shape", { ) ) ) + + # Check conversion with unnest=FALSE + usage_df_no_unnest <- as.data.frame(usage, unnest = FALSE) + expect_equal(names(usage_df_no_unnest), c("id", "user_guid", "content_guid", "timestamp", "data")) }) }) From cf48a08623643dc6737ba5ad22a7dfafd53dc62b Mon Sep 17 00:00:00 2001 From: Toph Allen Date: Fri, 18 Jul 2025 15:59:20 -0400 Subject: [PATCH 17/23] update _pkgdown.yml --- _pkgdown.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/_pkgdown.yml b/_pkgdown.yml index 00aa5680..3e0412b1 100644 --- a/_pkgdown.yml +++ b/_pkgdown.yml @@ -67,6 +67,8 @@ reference: Helpers to "get" data out of Connect contents: - starts_with("get") + - as.data.frame.connect_list_hits + - as_tibble.connect_list_hits" - title: "Other" desc: > From 442ac6296d210e365ea14bb4cad0577c02ec29cf Mon Sep 17 00:00:00 2001 From: Toph Allen Date: Mon, 21 Jul 2025 10:23:32 -0400 Subject: [PATCH 18/23] fix typo --- _pkgdown.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/_pkgdown.yml b/_pkgdown.yml index 3e0412b1..d8f943c8 100644 --- a/_pkgdown.yml +++ b/_pkgdown.yml @@ -68,7 +68,7 @@ reference: contents: - starts_with("get") - as.data.frame.connect_list_hits - - as_tibble.connect_list_hits" + - as_tibble.connect_list_hits - title: "Other" desc: > From b19f147c2205571068244c8d682757cdcbac2dff Mon Sep 17 00:00:00 2001 From: Toph Allen Date: Mon, 21 Jul 2025 10:47:38 -0400 Subject: [PATCH 19/23] fix tests --- tests/testthat/test-get.R | 23 +++++++++++-------- tests/testthat/test-parse.R | 46 ------------------------------------- 2 files changed, 13 insertions(+), 56 deletions(-) diff --git a/tests/testthat/test-get.R b/tests/testthat/test-get.R index c2968659..3bb7d14a 100644 --- a/tests/testthat/test-get.R +++ b/tests/testthat/test-get.R @@ -375,8 +375,8 @@ test_that("get_content only requests vanity URLs for Connect 2024.06.0 and up", }) }) -test_that("get_usage() returns usage data in the expected shape", { - with_mock_dir("2025.04.0", { +with_mock_dir("2025.04.0", { + test_that("get_usage() returns usage data in the expected shape", { client <- connect(server = "https://connect.example", api_key = "fake") usage <- get_usage( client, @@ -393,11 +393,13 @@ test_that("get_usage() returns usage data in the expected shape", { usage[[1]], list( id = 8966707L, - user_guid = NA_character_, + user_guid = NULL, content_guid = "475618c9", timestamp = "2025-04-30T12:49:16.269904Z", - path = "/hello", - user_agent = "Datadog/Synthetics" + data = list( + path = "/hello", + user_agent = "Datadog/Synthetics" + ) ) ) @@ -405,7 +407,7 @@ test_that("get_usage() returns usage data in the expected shape", { usage_df <- as.data.frame(usage) expect_equal( usage_df, - tibble::tibble( + data.frame( id = c(8966707L, 8966708L, 8967206L, 8967210L, 8966214L), user_guid = c(NA, NA, NA, NA, "fecbd383"), content_guid = c( @@ -437,12 +439,13 @@ test_that("get_usage() returns usage data in the expected shape", { # Check conversion with unnest=FALSE usage_df_no_unnest <- as.data.frame(usage, unnest = FALSE) - expect_equal(names(usage_df_no_unnest), c("id", "user_guid", "content_guid", "timestamp", "data")) + expect_equal( + names(usage_df_no_unnest), + c("id", "user_guid", "content_guid", "timestamp", "data") + ) }) -}) -test_that("Metrics firehose is called with expected parameters", { - with_mock_api({ + test_that("Metrics firehose is called with expected parameters", { client <- Connect$new(server = "https://connect.example", api_key = "fake") # $version is loaded lazily, we need it before calling get_usage() client$version diff --git a/tests/testthat/test-parse.R b/tests/testthat/test-parse.R index c9b93128..d6c91758 100644 --- a/tests/testthat/test-parse.R +++ b/tests/testthat/test-parse.R @@ -336,49 +336,3 @@ test_that("works for bad inputs", { expect_s3_class(res$start_time, "POSIXct") expect_s3_class(res$end_time, "POSIXct") }) - -test_that("fast_unnest_character() extracts a list column to character columns", { - df <- tibble::tibble(id = 1:2, animal = c("cat", "dog")) - df$info <- list( - list(path = "/a", user_agent = "ua1"), - list(path = "/b", user_agent = "ua2") - ) - - out <- fast_unnest_character(df, "info") - expect_named(out, c("id", "animal", "path", "user_agent")) - expect_equal(out$id, 1:2) - expect_equal(out$animal, c("cat", "dog")) - expect_equal(out$path, c("/a", "/b")) - expect_equal(out$user_agent, c("ua1", "ua2")) - expect_false("info" %in% names(out)) -}) - -test_that("fast_unnest_character() converts NULL to NA", { - df <- tibble::tibble(id = 1:3, animal = c("cat", "dog", "chinchilla")) - df$info <- list( - list(path = "/a", user_agent = NULL), - list(path = NULL, user_agent = "ua2"), - list(path = NULL, user_agent = NULL) - ) - - out <- fast_unnest_character(df, "info") - expect_equal(out$path, c("/a", NA_character_, NA_character_)) - expect_equal(out$user_agent, c(NA_character_, "ua2", NA_character_)) -}) - -test_that("fast_unnest_character errs when column doesn’t exist", { - df <- data.frame(x = 1:2) - expect_error( - fast_unnest_character(df, "missing_col"), - "col_name is not present in df" - ) -}) - -test_that("fast_unnest_character errs when column isn't a list", { - x <- 1 - df <- data.frame(x = 1:2) - expect_error( - fast_unnest_character(df, x), - "col_name must be a character vector" - ) -}) From 9d6cef403b35fe64cf3d90ba9a4ea16eb9b09db3 Mon Sep 17 00:00:00 2001 From: Toph Allen Date: Mon, 21 Jul 2025 11:07:09 -0400 Subject: [PATCH 20/23] fix for CI warning --- R/get.R | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/R/get.R b/R/get.R index 0f078c54..7f982d5c 100644 --- a/R/get.R +++ b/R/get.R @@ -644,7 +644,7 @@ as.data.frame.connect_list_hits <- function(x, unnest = TRUE, ...) { call. = FALSE ) } - usage_df <- tidyr::unnest_wider(usage_df, data, ptype = list(path = character(0), user_agent = character(0))) + usage_df <- tidyr::unnest_wider(usage_df, "data", ptype = list(path = character(0), user_agent = character(0))) } as.data.frame(usage_df) } From 68e45864eaa1ca75a58c79fe80f77672110b30e5 Mon Sep 17 00:00:00 2001 From: Toph Allen Date: Mon, 21 Jul 2025 11:19:25 -0400 Subject: [PATCH 21/23] fix as.data.frame method for S3 generic conformance --- R/get.R | 16 +++++++++++++--- man/as.data.frame.connect_list_hits.Rd | 6 +++--- 2 files changed, 16 insertions(+), 6 deletions(-) diff --git a/R/get.R b/R/get.R index 7f982d5c..56ec6ea5 100644 --- a/R/get.R +++ b/R/get.R @@ -635,7 +635,13 @@ get_usage <- function(client, from = NULL, to = NULL) { #' @return A `data.frame` with one row per usage record. #' @export #' @method as.data.frame connect_list_hits -as.data.frame.connect_list_hits <- function(x, unnest = TRUE, ...) { +as.data.frame.connect_list_hits <- function( + x, + row.names = NULL, + optional = FALSE, + ..., + unnest = TRUE +) { usage_df <- parse_connectapi_typed(x, connectapi_ptypes$usage) if (unnest) { if (!requireNamespace("tidyr", quietly = TRUE)) { @@ -644,9 +650,13 @@ as.data.frame.connect_list_hits <- function(x, unnest = TRUE, ...) { call. = FALSE ) } - usage_df <- tidyr::unnest_wider(usage_df, "data", ptype = list(path = character(0), user_agent = character(0))) + usage_df <- tidyr::unnest_wider( + usage_df, + "data", + ptype = list(path = character(0), user_agent = character(0)) + ) } - as.data.frame(usage_df) + as.data.frame(usage_df, row.names = row.names, optional = optional, ...) } #' Convert usage data to a tibble diff --git a/man/as.data.frame.connect_list_hits.Rd b/man/as.data.frame.connect_list_hits.Rd index eea7d41d..f05fa100 100644 --- a/man/as.data.frame.connect_list_hits.Rd +++ b/man/as.data.frame.connect_list_hits.Rd @@ -4,15 +4,15 @@ \alias{as.data.frame.connect_list_hits} \title{Convert usage data to a data frame} \usage{ -\method{as.data.frame}{connect_list_hits}(x, unnest = TRUE, ...) +\method{as.data.frame}{connect_list_hits}(x, row.names = NULL, optional = FALSE, ..., unnest = TRUE) } \arguments{ \item{x}{A \code{connect_list_hits} object (from \code{\link[=get_usage]{get_usage()}}).} +\item{...}{Ignored.} + \item{unnest}{Logical; if \code{TRUE} (default), extracts nested fields using \pkg{tidyr}. Set to \code{FALSE} to skip unnesting.} - -\item{...}{Ignored.} } \value{ A \code{data.frame} with one row per usage record. From fa241997757bac657eac0bd1ec8d40386e27dd2c Mon Sep 17 00:00:00 2001 From: Toph Allen Date: Mon, 21 Jul 2025 11:29:28 -0400 Subject: [PATCH 22/23] update documentation to match --- R/get.R | 4 +++- man/as.data.frame.connect_list_hits.Rd | 6 +++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/R/get.R b/R/get.R index 56ec6ea5..b511c4f1 100644 --- a/R/get.R +++ b/R/get.R @@ -628,9 +628,11 @@ get_usage <- function(client, from = NULL, to = NULL) { #' field, if available. #' #' @param x A `connect_list_hits` object (from [get_usage()]). +#' @param row.names Passed to [base::as.data.frame()]. +#' @param optional Passed to [base::as.data.frame()]. +#' @param ... Passed to [base::as.data.frame()]. #' @param unnest Logical; if `TRUE` (default), extracts nested fields using #' \pkg{tidyr}. Set to `FALSE` to skip unnesting. -#' @param ... Ignored. #' #' @return A `data.frame` with one row per usage record. #' @export diff --git a/man/as.data.frame.connect_list_hits.Rd b/man/as.data.frame.connect_list_hits.Rd index f05fa100..7bf65006 100644 --- a/man/as.data.frame.connect_list_hits.Rd +++ b/man/as.data.frame.connect_list_hits.Rd @@ -9,7 +9,11 @@ \arguments{ \item{x}{A \code{connect_list_hits} object (from \code{\link[=get_usage]{get_usage()}}).} -\item{...}{Ignored.} +\item{row.names}{Passed to \code{\link[base:as.data.frame]{base::as.data.frame()}}.} + +\item{optional}{Passed to \code{\link[base:as.data.frame]{base::as.data.frame()}}.} + +\item{...}{Passed to \code{\link[base:as.data.frame]{base::as.data.frame()}}.} \item{unnest}{Logical; if \code{TRUE} (default), extracts nested fields using \pkg{tidyr}. Set to \code{FALSE} to skip unnesting.} From b709457a179592759d581f548ad738f3ba999b9a Mon Sep 17 00:00:00 2001 From: Toph Allen Date: Mon, 21 Jul 2025 11:38:51 -0400 Subject: [PATCH 23/23] fix lint --- R/get.R | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/R/get.R b/R/get.R index b511c4f1..cc3fa78d 100644 --- a/R/get.R +++ b/R/get.R @@ -639,7 +639,7 @@ get_usage <- function(client, from = NULL, to = NULL) { #' @method as.data.frame connect_list_hits as.data.frame.connect_list_hits <- function( x, - row.names = NULL, + row.names = NULL, # nolint optional = FALSE, ..., unnest = TRUE