Skip to content

Commit a53f5f0

Browse files
authored
content with issues prototype: table view (#33)
* failed content jobs static table view * adjust jobs filter so all failed jobs returned * map additional jobs types & exit codes * group job types, exit codes, remove visitor * adjust title, comments * adjust extension toml description
1 parent d023d6a commit a53f5f0

File tree

4 files changed

+1066
-0
lines changed

4 files changed

+1066
-0
lines changed
Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
library(shiny)
2+
library(bslib)
3+
library(DT)
4+
library(connectapi)
5+
library(dplyr)
6+
library(purrr)
7+
8+
# cache data to disk with a refresh every 8h
9+
shinyOptions(
10+
cache = cachem::cache_disk("./app_cache/cache/", max_age = 60 * 60 * 8)
11+
)
12+
13+
source("get_usage.R")
14+
15+
# Hacky function to get a list of Content class objects without making a request
16+
# for each item. These objects differ from the ones created by `content_item()`
17+
# because they also include the full owner info as returned by `get_content()`.
18+
as_content_list <- function(content_df, client) {
19+
cdf_split <- split(content_df, 1:nrow(content_df))
20+
map(cdf_split, function(x) {
21+
x <- x[, !(names(x) %in% c("tags"))]
22+
x <- as.list(x)
23+
Content$new(client, x)
24+
})
25+
}
26+
27+
# checks to see if a content item has failed jobs, grabs usage data if it does,
28+
# then compiles content, job, and usage data together, returning it.
29+
get_failed_job_data <- function(item, usage) {
30+
failed_jobs <- tryCatch(
31+
{
32+
get_jobs(item) |>
33+
# filter successful jobs
34+
filter(exit_code != 0) |>
35+
# map content job types to something more readable
36+
mutate(tag = case_when(
37+
tag %in% c("build_report", "build_site", "build_jupyter") ~ "Building",
38+
tag %in% c("packrat_restore", "python_restore") ~ "Restoring environment",
39+
tag == "configure_report" ~ "Configuring report",
40+
tag %in% c("run_app",
41+
"run_api",
42+
"run_tensorflow",
43+
"run_python_api",
44+
"run_dash_app",
45+
"run_gradio_app",
46+
"run_streamlit",
47+
"run_bokeh_app",
48+
"run_fastapi_app",
49+
"run_voila_app",
50+
"run_pyshiny_app") ~ "Running",
51+
tag == "render_shiny" ~ "Rendering",
52+
tag == "ctrl_extraction" ~ "Extracting parameters",
53+
TRUE ~ tag)) |>
54+
# map exit codes to something more readable
55+
mutate(exit_code = as.character(exit_code)) |>
56+
mutate(exit_code = case_when(
57+
exit_code %in% c("1", "2", "134") ~ "failed to run / error during running",
58+
exit_code == "137" ~ "out of memory",
59+
exit_code %in% c("255", "15", "130") ~ "process terminated by server",
60+
exit_code %in% c("13", "127") ~ "configuration / permissions error",
61+
TRUE ~ exit_code))
62+
},
63+
error = function(e) {
64+
# content item does not have any jobs
65+
NULL
66+
}
67+
)
68+
69+
if (is.null(failed_jobs) || nrow(failed_jobs) == 0) {
70+
return(NULL)
71+
} else {
72+
# handle content without usage data, such as unpublished content
73+
last_visit <- usage %>%
74+
filter(content_guid == item$content$guid) %>%
75+
slice_max(timestamp) %>%
76+
select(timestamp)
77+
if (is.na(item$content$title)) {
78+
item$content$title <- "" # use empty strings when content is missing title
79+
}
80+
# return required information from https://github.com/posit-dev/connect/issues/30288
81+
all_failed_jobs <- bind_rows(lapply(seq_len(nrow(failed_jobs)), function(i) {
82+
tibble(
83+
"content_title" = item$content$title,
84+
"content_guid" = item$content$guid,
85+
"content_owner" = item$content$owner[[1]]$username,
86+
"job_failed_at" = failed_jobs$end_time[i],
87+
"failed_job_type" = failed_jobs$tag[i],
88+
"failure_reason" = failed_jobs$exit_code[i],
89+
"last_deployed_time" = item$content$last_deployed_time,
90+
"last_visited" = as.POSIXct(last_visit$timestamp)
91+
)
92+
}))
93+
all_failed_jobs
94+
}
95+
}
96+
97+
server <- function(input, output, session) {
98+
# initialize Connect API client
99+
client <- connect()
100+
101+
# get content once up front and pass it around for additional filtering
102+
content <- get_content(client, limit = inf)
103+
104+
# cache content list
105+
content_list <- reactive({
106+
as_content_list(content, client)
107+
}) |> bindCache("static_key")
108+
109+
# cache usage (uses firehose if available, legacy otherwise)
110+
usage <- reactive({
111+
get_usage(client)
112+
}) |> bindCache("static_key")
113+
114+
# cache content with failed jobs
115+
bad_content_df <- reactive({
116+
req(content_list(), usage())
117+
map_dfr(content_list(), ~ get_failed_job_data(.x, usage()))
118+
}) |> bindCache("static_key")
119+
120+
# output the datatable of failed jobs
121+
output$jobs <- renderDT({
122+
datatable(bad_content_df(),
123+
rownames = FALSE,
124+
escape = FALSE,
125+
options = list( # non-interactive table for this prototype
126+
paging = FALSE,
127+
searching = FALSE,
128+
ordering = FALSE,
129+
info = FALSE,
130+
dom = "t"
131+
)
132+
)
133+
})
134+
}
135+
136+
ui <- fluidPage(
137+
fluidRow(
138+
column(12,
139+
titlePanel("Content With Issues (table view)")
140+
)
141+
),
142+
143+
fluidRow(
144+
column(12,
145+
titlePanel(tags$h6("All failed content jobs:")),
146+
DTOutput("jobs"),
147+
)
148+
)
149+
)
150+
151+
152+
shinyApp(ui, server)
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
name = "prototype-content-with-issues-table"
2+
title = "Content With Issues (table view)"
3+
description = "R Shiny prototype extension that displays a table of all failed content jobs."
4+
access_type = "acl"
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
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+
tryCatch(
49+
{
50+
print("Trying firehose usage endpoint.")
51+
get_usage_firehose(client, from, to)
52+
},
53+
error = function(e) {
54+
print("Could not use firehose endpoint; trying legacy usage endpoints.")
55+
get_usage_legacy(client, from, to)
56+
}
57+
)
58+
}

0 commit comments

Comments
 (0)