From 99e5a1014fb30a37a915f9981735c8b35e38f0d0 Mon Sep 17 00:00:00 2001 From: Toph Allen Date: Tue, 29 Apr 2025 11:29:23 -0400 Subject: [PATCH 1/8] 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 2/8] 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 3/8] 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 4/8] 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 5/8] 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 6/8] 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 7/8] 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 8/8] 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