Skip to content

Commit 50668cf

Browse files
authored
who is visiting this content (table view) (#38)
* first commit * fix summary sentence * clarify comment * add date formatting and column renaming
1 parent 43cfec3 commit 50668cf

File tree

9 files changed

+3724
-0
lines changed

9 files changed

+3724
-0
lines changed
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
source("renv/activate.R")
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
.posit/
2+
app_cache/
Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
library(shiny)
2+
library(bslib)
3+
library(shinyjs)
4+
library(connectapi)
5+
library(dplyr)
6+
library(glue)
7+
library(lubridate)
8+
library(tidyr)
9+
10+
shinyOptions(
11+
cache = cachem::cache_disk("./app_cache/cache/", max_age = 60 * 60 * 8)
12+
)
13+
14+
source("get_usage.R")
15+
16+
ui <- page_fillable(
17+
useShinyjs(),
18+
theme = bs_theme(version = 5),
19+
20+
card(
21+
card_header("Who is Visiting This Content?"),
22+
layout_sidebar(
23+
sidebar = sidebar(
24+
title = "No Filters Yet",
25+
open = FALSE,
26+
27+
actionButton("clear_cache", "Clear Cache", icon = icon("refresh"))
28+
),
29+
30+
textInput(
31+
"content_guid",
32+
"Content GUID"
33+
),
34+
35+
h4(
36+
id = "guid_input_msg",
37+
"Please enter a content GUID"
38+
),
39+
40+
textOutput("summary_message"),
41+
42+
tabsetPanel(
43+
id = "content_visit_tables",
44+
tabPanel(
45+
"List of Visits",
46+
tableOutput("all_visits")
47+
),
48+
tabPanel(
49+
"Aggregated Visits",
50+
tableOutput("aggregated_visits")
51+
)
52+
)
53+
)
54+
)
55+
)
56+
57+
server <- function(input, output, session) {
58+
# Cache invalidation button ----
59+
cache <- cachem::cache_disk("./app_cache/cache/")
60+
observeEvent(input$clear_cache, {
61+
print("Cache cleared!")
62+
cache$reset() # Clears all cached data
63+
session$reload() # Reload the app to ensure fresh data
64+
})
65+
66+
observe({
67+
if (nchar(input$content_guid) == 0) {
68+
show("guid_input_msg")
69+
hide("content_visit_tables")
70+
} else {
71+
hide("guid_input_msg")
72+
show("content_visit_tables")
73+
}
74+
})
75+
76+
# Loading and processing data ----
77+
client <- connect()
78+
79+
# Default dates. "This week" is best "common sense" best represented by six
80+
# days ago thru the end of today. Without these, content takes too long to
81+
# display on some servers.
82+
date_range <- reactive({
83+
list(
84+
from_date = today() - ddays(6),
85+
to_date = today()
86+
)
87+
})
88+
89+
content <- reactive({
90+
# Grab the entire content data frame here and filter it using the pasted-in
91+
# GUID to obtain content title and other metadata, rather than making a
92+
# request to `v1/content/{GUID}`. If this were a prod, standalone dashboard,
93+
# might be better to call that endpoint.
94+
get_content(client)
95+
}) |> bindCache("static_key")
96+
97+
user_names <- reactive({
98+
get_users(client) |>
99+
mutate(full_name = paste(first_name, last_name)) |>
100+
select(user_guid = guid, full_name, username)
101+
}) |> bindCache("static_key")
102+
103+
usage_data <- reactive({
104+
get_usage(
105+
client,
106+
from = date_range()$from_date,
107+
to = date_range()$to_date + hours(23) + minutes(59) + seconds(59)
108+
)
109+
}) |> bindCache(date_range()$from_date, date_range()$to_date)
110+
111+
# Compute data
112+
all_visits_data <- reactive({
113+
usage_data() |>
114+
filter(content_guid == input$content_guid) |>
115+
left_join(user_names(), by = "user_guid") |>
116+
replace_na(list(full_name = "[Anonymous]")) |>
117+
arrange(desc(timestamp)) |>
118+
select(timestamp, full_name, username)
119+
}) |> bindCache(date_range()$from_date, date_range()$to_date, input$content_guid)
120+
121+
aggregated_visits_data <- reactive({
122+
usage_data() |>
123+
filter(content_guid == input$content_guid) |>
124+
group_by(user_guid) |>
125+
summarize(n_visits = n()) |>
126+
left_join(user_names(), by = "user_guid") |>
127+
replace_na(list(full_name = "[Anonymous]")) |>
128+
arrange(desc(n_visits)) |>
129+
select(n_visits, full_name, username)
130+
}) |> bindCache(date_range()$from_date, date_range()$to_date, input$content_guid)
131+
132+
summary_message <- reactive({
133+
content_title <- content() |>
134+
filter(guid == input$content_guid) |>
135+
pull(title)
136+
hits <- all_visits_data()
137+
glue(
138+
"Content '{content_title}' had {nrow(hits)} between ",
139+
"{min(hits$timestamp)} and {max(hits$timestamp)}."
140+
)
141+
})
142+
143+
144+
output$summary_message <- renderText(summary_message())
145+
output$all_visits <- renderTable(
146+
all_visits_data() |>
147+
transmute(timestamp = format(timestamp, "%Y-%m-%d %H:%M:%S"), full_name, username) |>
148+
rename("Time" = timestamp, "Full Name" = full_name, "Username" = username)
149+
)
150+
output$aggregated_visits <- renderTable(
151+
aggregated_visits_data() |>
152+
rename("Total Visits" = n_visits, "Full Name" = full_name, "Username" = username)
153+
)
154+
}
155+
156+
shinyApp(ui, server)
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
library(connectapi)
2+
3+
# This file contains functions that ultimately will more likely be part of
4+
# connectapi. As such, I'm not using dplyr or pipes here.
5+
6+
NA_datetime_ <- vctrs::new_datetime(NA_real_, tzone = "UTC")
7+
NA_list_ <- list(list())
8+
9+
usage_dtype <- tibble::tibble(
10+
"id" = NA_integer_,
11+
"user_guid" = NA_character_,
12+
"content_guid" = NA_character_,
13+
"timestamp" = NA_datetime_,
14+
"data" = NA_list_
15+
)
16+
17+
# A rough implementation of how a new firehose usage function would work in
18+
# `connectapi`.
19+
get_usage_firehose <- function(client, from = NULL, to = NULL) {
20+
usage_raw <- client$GET(
21+
connectapi:::unversioned_url("instrumentation", "content", "hits"),
22+
query = list(
23+
from = from,
24+
to = to
25+
)
26+
)
27+
28+
# FIXME for connectapi: This is slow, it's where most of the slowness is with
29+
# the new endpoint.
30+
usage_parsed <- connectapi:::parse_connectapi_typed(usage_raw, usage_dtype)
31+
32+
usage_parsed[c("user_guid", "content_guid", "timestamp")]
33+
}
34+
35+
get_usage_legacy <- function(client, from = NULL, to = NULL) {
36+
shiny_usage <- get_usage_shiny(client, limit = Inf, from = from, to = to)
37+
shiny_usage_cols <- shiny_usage[c("user_guid", "content_guid")]
38+
shiny_usage_cols$timestamp <- shiny_usage$started
39+
40+
static_usage <- get_usage_static(client, limit = Inf, from = from, to = to)
41+
static_usage_cols <- static_usage[c("user_guid", "content_guid")]
42+
static_usage_cols$timestamp <- static_usage$time
43+
44+
bind_rows(shiny_usage_cols, static_usage_cols)
45+
}
46+
47+
get_usage <- function(client, from = NULL, to = NULL) {
48+
from <- format(from, "%Y-%m-%dT%H:%M:%SZ")
49+
to <- format(to, "%Y-%m-%dT%H:%M:%SZ")
50+
tryCatch(
51+
{
52+
print("Trying firehose usage endpoint.")
53+
get_usage_firehose(client, from, to)
54+
},
55+
error = function(e) {
56+
print("Could not use firehose endpoint; trying legacy usage endpoints.")
57+
get_usage_legacy(client, from, to)
58+
}
59+
)
60+
}

0 commit comments

Comments
 (0)