Skip to content

Commit 2c82d16

Browse files
feat: support new metrics firehose api with get_usage() (#404)
Co-authored-by: Neal Richardson <neal.p.richardson@gmail.com>
1 parent 4e8ffa2 commit 2c82d16

File tree

15 files changed

+482
-8
lines changed

15 files changed

+482
-8
lines changed

.lintr

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
linters: linters_with_defaults(
22
line_length_linter = line_length_linter(120L),
33
object_name_linter = object_name_linter(styles = c("snake_case", "symbols", "CamelCase")),
4-
cyclocomp_linter = cyclocomp_linter(30L),
4+
cyclocomp_linter = NULL, # Issues with R6 classes.
55
object_length_linter(32L),
66
indentation_linter = indentation_linter(hanging_indent_style = "tidy"),
77
return_linter = NULL

DESCRIPTION

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ Suggests:
5757
rsconnect,
5858
spelling,
5959
testthat,
60+
tidyr,
6061
webshot2,
6162
withr
6263
VignetteBuilder:

NAMESPACE

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,9 @@ S3method("[",connect_tag_tree)
55
S3method("[[",connect_tag_tree)
66
S3method(api_build,op_base_connect)
77
S3method(api_build,op_head)
8+
S3method(as.data.frame,connect_list_hits)
89
S3method(as.data.frame,tbl_connect)
10+
S3method(as_tibble,connect_list_hits)
911
S3method(connect_vars,op_base)
1012
S3method(connect_vars,op_single)
1113
S3method(connect_vars,tbl_connect)
@@ -97,6 +99,7 @@ export(get_tag_data)
9799
export(get_tags)
98100
export(get_thumbnail)
99101
export(get_timezones)
102+
export(get_usage)
100103
export(get_usage_shiny)
101104
export(get_usage_static)
102105
export(get_user_permission)
@@ -159,6 +162,7 @@ importFrom(rlang,"%||%")
159162
importFrom(rlang,":=")
160163
importFrom(rlang,arg_match)
161164
importFrom(rlang,is_string)
165+
importFrom(tibble,as_tibble)
162166
importFrom(utils,browseURL)
163167
importFrom(utils,capture.output)
164168
importFrom(utils,compareVersion)

NEWS.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
11
# connectapi (development version)
22

3+
## New features
4+
5+
- New `get_usage()` function returns content usage data from Connect's `GET
6+
v1/instrumentation/content/hits` endpoint on Connect v2025.04.0 and higher.
7+
(#390)
38

49
## Enhancements and fixes
510

R/connect.R

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -858,7 +858,9 @@ Connect <- R6::R6Class(
858858
docs = function(docs = "api", browse = TRUE) {
859859
stopifnot(docs %in% c("admin", "user", "api"))
860860
url <- paste0(self$server, "/__docs__/", docs)
861-
if (browse) utils::browseURL(url)
861+
if (browse) {
862+
utils::browseURL(url)
863+
}
862864
url
863865
},
864866

R/get.R

Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -526,6 +526,157 @@ get_usage_static <- function(
526526
return(out)
527527
}
528528

529+
#' Get usage information for deployed content
530+
#'
531+
#' @description
532+
#' Retrieve content hits for all available content on the server. Available
533+
#' content depends on the user whose API key is in use. Administrator accounts
534+
#' will receive data for all content on the server. Publishers will receive data
535+
#' for all content they own or collaborate on.
536+
#'
537+
#' If no date-times are provided, all usage data will be returned.
538+
#'
539+
#' @param client A `Connect` R6 client object.
540+
#' @param from Optional date-time (`POSIXct` or `POSIXlt`). Only
541+
#' records after this time are returned. If not provided, records
542+
#' are returned back to the first record available.
543+
#' @param to Optional date-time (`POSIXct` or `POSIXlt`). Only records
544+
#' before this time are returned. If not provided, all records up to
545+
#' the most recent are returned.
546+
#'
547+
#' @return A list of usage records. Each record is a list with all elements
548+
#' as character strings unless otherwise specified.
549+
#'
550+
#' * `id`: An integer identifier for the hit.
551+
#' * `user_guid`: The user GUID if the visitor is logged-in, `NULL` for
552+
#' anonymous hits.
553+
#' * `content_guid`: The GUID of the visited content.
554+
#' * `timestamp`: The time of the hit in RFC3339 format.
555+
#' * `data`: A nested list with optional fields:
556+
#' * `path`: The request path (if recorded).
557+
#' * `user_agent`: The user agent string (if available).
558+
#'
559+
#' Use [as.data.frame()] or [tibble::as_tibble()] to convert to a flat
560+
#' table with parsed types. In the resulting data frame:
561+
#'
562+
#' * `timestamp` is parsed to `POSIXct`.
563+
#' * `path` and `user_agent` are extracted from the nested `data` field.
564+
#'
565+
#' By default, [as.data.frame()] attempts to extract the nested fields using
566+
#' the \pkg{tidyr} package. If \pkg{tidyr} is not available, or if you want to
567+
#' skip unnesting, call `as.data.frame(x, unnest = FALSE)` to leave `data` as
568+
#' a list-column.
569+
#'
570+
#' @details
571+
#'
572+
#' The data returned by `get_usage()` includes all content types. For Shiny
573+
#' content, the `timestamp` indicates the *start* of the Shiny session.
574+
#' Additional fields for Shiny and non-Shiny are available respectively from
575+
#' [get_usage_shiny()] and [get_usage_static()]. `get_usage_shiny()` includes a
576+
#' field for the session end time; `get_usage_static()` includes variant,
577+
#' rendering, and bundle identifiers for the visited content.
578+
#'
579+
#' When possible, however, we recommend using `get_usage()` over
580+
#' `get_usage_static()` or `get_usage_shiny()`, as it is faster and more efficient.
581+
#'
582+
#' @seealso [as.data.frame.connect_list_hits()], [as_tibble.connect_list_hits()]
583+
#'
584+
#' @examples
585+
#' \dontrun{
586+
#' client <- connect()
587+
#'
588+
#' # Fetch the last 2 days of hits
589+
#' usage <- get_usage(client, from = Sys.Date() - 2, to = Sys.Date())
590+
#'
591+
#' # Fetch usage after a specified date and convert to a data frame.
592+
#' usage <- get_usage(
593+
#' client,
594+
#' from = as.POSIXct("2025-05-02 12:40:00", tz = "UTC")
595+
#' )
596+
#'
597+
#' # Fetch all usage
598+
#' usage <- get_usage(client)
599+
#'
600+
#' # Convert to tibble or data frame
601+
#' usage_df <- tibble::as_tibble(usage)
602+
#'
603+
#' # Skip unnesting if tidyr is not installed
604+
#' usage_df <- as.data.frame(usage, unnest = FALSE)
605+
#' }
606+
#'
607+
#' @export
608+
get_usage <- function(client, from = NULL, to = NULL) {
609+
error_if_less_than(client$version, "2025.04.0")
610+
611+
usage <- client$GET(
612+
v1_url("instrumentation", "content", "hits"),
613+
query = list(
614+
from = make_timestamp(from),
615+
to = make_timestamp(to)
616+
)
617+
)
618+
619+
class(usage) <- c("connect_list_hits", class(usage))
620+
usage
621+
}
622+
623+
#' Convert usage data to a data frame
624+
#'
625+
#' @description
626+
#' Converts an object returned by [get_usage()] into a data frame with parsed
627+
#' column types. By default, extracts `path` and `user_agent` from the `data`
628+
#' field, if available.
629+
#'
630+
#' @param x A `connect_list_hits` object (from [get_usage()]).
631+
#' @param row.names Passed to [base::as.data.frame()].
632+
#' @param optional Passed to [base::as.data.frame()].
633+
#' @param ... Passed to [base::as.data.frame()].
634+
#' @param unnest Logical; if `TRUE` (default), extracts nested fields using
635+
#' \pkg{tidyr}. Set to `FALSE` to skip unnesting.
636+
#'
637+
#' @return A `data.frame` with one row per usage record.
638+
#' @export
639+
#' @method as.data.frame connect_list_hits
640+
as.data.frame.connect_list_hits <- function(
641+
x,
642+
row.names = NULL, # nolint
643+
optional = FALSE,
644+
...,
645+
unnest = TRUE
646+
) {
647+
usage_df <- parse_connectapi_typed(x, connectapi_ptypes$usage)
648+
if (unnest) {
649+
if (!requireNamespace("tidyr", quietly = TRUE)) {
650+
stop(
651+
"`unnest = TRUE` requires tidyr. Install tidyr or set `unnest = FALSE`.",
652+
call. = FALSE
653+
)
654+
}
655+
usage_df <- tidyr::unnest_wider(
656+
usage_df,
657+
"data",
658+
ptype = list(path = character(0), user_agent = character(0))
659+
)
660+
}
661+
as.data.frame(usage_df, row.names = row.names, optional = optional, ...)
662+
}
663+
664+
#' Convert usage data to a tibble
665+
#'
666+
#' @description
667+
#' Converts an object returned by [get_usage()] to a tibble via
668+
#' [as.data.frame.connect_list_hits()].
669+
#'
670+
#' @param x A `connect_list_hits` object.
671+
#' @param ... Passed to [as.data.frame()].
672+
#'
673+
#' @return A tibble with one row per usage record.
674+
#' @export
675+
#' @importFrom tibble as_tibble
676+
#' @method as_tibble connect_list_hits
677+
as_tibble.connect_list_hits <- function(x, ...) {
678+
tibble::as_tibble(as.data.frame(x, ...))
679+
}
529680

530681
#' Get Audit Logs from Posit Connect Server
531682
#'

R/parse.R

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,15 +58,19 @@ ensure_column <- function(data, default, name) {
5858
# manual fix because vctrs::vec_cast cannot cast double -> datetime or char -> datetime
5959
col <- coerce_datetime(col, default, name = name)
6060
}
61+
6162
if (inherits(default, "fs_bytes") && !inherits(col, "fs_bytes")) {
6263
col <- coerce_fsbytes(col, default)
6364
}
65+
6466
if (inherits(default, "integer64") && !inherits(col, "integer64")) {
6567
col <- bit64::as.integer64(col)
6668
}
69+
6770
if (inherits(default, "list") && !inherits(col, "list")) {
6871
col <- list(col)
6972
}
73+
7074
col <- vctrs::vec_cast(col, default, x_arg = name)
7175
}
7276
data[[name]] <- col
@@ -101,6 +105,7 @@ parse_connectapi <- function(data) {
101105
))
102106
}
103107

108+
104109
coerce_fsbytes <- function(x, to, ...) {
105110
if (is.numeric(x)) {
106111
fs::as_fs_bytes(x)

R/ptype.R

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
NA_datetime_ <- # nolint: object_name_linter
2-
vctrs::new_datetime(NA_real_, tzone = "UTC")
2+
vctrs::new_datetime(NA_real_, tzone = Sys.timezone())
33
NA_list_ <- # nolint: object_name_linter
44
list(list())
55

@@ -38,6 +38,13 @@ connectapi_ptypes <- list(
3838
"bundle_id" = NA_character_,
3939
"data_version" = NA_integer_
4040
),
41+
usage = tibble::tibble(
42+
"id" = NA_integer_,
43+
"user_guid" = NA_character_,
44+
"content_guid" = NA_character_,
45+
"timestamp" = NA_datetime_,
46+
"data" = NA_list_
47+
),
4148
content = tibble::tibble(
4249
"guid" = NA_character_,
4350
"name" = NA_character_,

_pkgdown.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,8 @@ reference:
6767
Helpers to "get" data out of Connect
6868
contents:
6969
- starts_with("get")
70+
- as.data.frame.connect_list_hits
71+
- as_tibble.connect_list_hits
7072

7173
- title: "Other"
7274
desc: >

man/as.data.frame.connect_list_hits.Rd

Lines changed: 28 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)