-
Notifications
You must be signed in to change notification settings - Fork 3
Connect User Metrics #181
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Connect User Metrics #181
Changes from 20 commits
aac0e77
ad72f9d
6820cb0
627dabf
93a2a8a
94e6b43
e8baeaa
6ac0883
7107381
42436cf
4572c0f
8ba0141
4d0401a
d9d70f7
a1861d1
18ee46d
18e8d93
e772ebc
6133051
2a282bb
499ba46
be09423
bba6a4f
f31a566
3601137
ddf8e3a
c772ce2
2baadba
3363159
d1961cf
83a9fc9
4803442
9c357c2
7fb37de
e3490d2
f31f418
78b7868
95f7b20
583691c
33fbddf
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,19 @@ | ||
| if (file.exists("renv")) { | ||
| source("renv/activate.R") | ||
| } else { | ||
| # The `renv` directory is automatically skipped when deploying with rsconnect. | ||
| message("No 'renv' directory found; renv won't be activated.") | ||
| } | ||
|
|
||
| # Allow absolute module imports (relative to the app root). | ||
| options(box.path = getwd()) | ||
|
|
||
| if (nzchar(system.file(package = "box.lsp"))) { | ||
| options( | ||
| languageserver.parser_hooks = list( | ||
| "box::use" = box.lsp::box_use_parser | ||
| ) | ||
| ) | ||
| } | ||
|
|
||
| options(repos = c(CRAN = "https://packagemanager.posit.co/cran/latest")) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,4 @@ | ||
| .Renviron | ||
| .Rproj.user | ||
| .Rhistory | ||
| .DS_Store |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| linters: | ||
| linters_with_defaults( | ||
| defaults = box.linters::rhino_default_linters, | ||
| line_length_linter = line_length_linter(100) | ||
| ) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,3 @@ | ||
| # Only use `dependencies.R` to infer project dependencies. | ||
| * | ||
| !dependencies.R |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,7 @@ | ||
| .github | ||
| .lintr | ||
| .renvignore | ||
| .Renviron | ||
| .rhino | ||
| .rscignore | ||
| tests |
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
| @@ -0,0 +1,55 @@ | ||||||
| # Connect Insights Dashboard | ||||||
|
|
||||||
| ## Environment Variables | ||||||
|
|
||||||
| This application requires the following environment variables to be set: | ||||||
|
|
||||||
| - `CONNECT_API_KEY` | ||||||
| - `CONNECT_SERVER` | ||||||
|
|
||||||
| By default, Posit Connect provides values for these variables, as outlined in the [Vars (Environment Variables) Section][User Guide Vars]. | ||||||
| However, there are cases where you might want to set these variables manually: | ||||||
|
|
||||||
| - If you want to retrieve data for applications deployed by a different Publisher than the one | ||||||
| deploying the User Metrics application, set `CONNECT_API_KEY` with that Publisher's API key. | ||||||
| - If you want to retrieve data from a different Posit Connect instance than the one where the User | ||||||
| Metrics application is deployed, set `CONNECT_SERVER` with the URL of that instance. | ||||||
| Additionally, `CONNECT_API_KEY` must be set to authenticate on the instance specified in `CONNECT_SERVER`. | ||||||
|
|
||||||
| ## Disclaimer | ||||||
|
|
||||||
| Posit Connect usage data is most accurate for applications accessed by authenticated users. | ||||||
| Unauthenticated users cannot be distinguished, and will be seen in the app as "Unknown user". | ||||||
|
|
||||||
| Read more: [_Why You Should Use Posit Connect Authentication And How to Set It Up_][rsconnect-auth]. | ||||||
|
|
||||||
| ## Troubleshooting | ||||||
|
|
||||||
| ### Posit Connect does not appear to have `CONNECT_SERVER` and `CONNECT_API_KEY` set | ||||||
|
|
||||||
| Per the [Configuration appendix] in the Posit Connect Admin Guide, these variables are set by default. | ||||||
| However, this behavior can be overridden via [DefaultServerEnv] and [DefaultAPIKeyEnv]. | ||||||
|
|
||||||
| Check with your Posit Connect administrator if that's the case. | ||||||
|
|
||||||
| ### The API connection fails due to a timeout after deploying the User Metrics application | ||||||
|
|
||||||
| If the connection times out using the default environment variables, the issue may be that the server cannot resolve its own fully qualified domain name. | ||||||
|
|
||||||
| To fix this, go to the User Metrics [application Vars][User Guide Vars] and set `CONNECT_SERVER` to a local address, e.g. `http://localhost:3939`. | ||||||
|
|
||||||
| (Note: the scheme in the URL is required by `connectapi::connect()`.) | ||||||
|
|
||||||
| ### There is no usage data for my application | ||||||
|
|
||||||
| As with environment variables, the [Instrumentation] feature is also configurable. | ||||||
|
|
||||||
| Confirm with your Posit Connect admin that instrumentation is enabled. | ||||||
|
|
||||||
| <!-- Links --> | ||||||
| [User Guide Vars]: https://docs.posit.com/connect/user/content-settings/#content-vars | ||||||
|
||||||
| [User Guide Vars]: https://docs.posit.com/connect/user/content-settings/#content-vars | |
| [User Guide Vars]: https://docs.posit.co/connect/user/content-settings/#content-vars |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nice catch!
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,63 @@ | ||
| meta: | ||
| app_title: "Posit Connect User Metrics" | ||
| credits: | ||
| enabled: TRUE | ||
| about: | ||
| references: | ||
| homepage: | ||
| name: "Link to Appsilon" | ||
| link: "https://go.appsilon.com/appsilon-user-metrics-app" | ||
| powered_by: | ||
| rhino: | ||
| name: "Rhino" | ||
| link: "https://go.appsilon.com/github-rhino-user-metrics-app" | ||
| img_name: "rhino.png" | ||
| desc: "Rhino is an Open-Source Package developed by Appsilon to | ||
| help the R community make more professional Shiny Apps. Rhino allows you to | ||
| create Shiny apps The Appsilon Way - like a fullstack software engineer. | ||
| Apply best software engineering practices, modularize your code, | ||
| test it well, make UI beautiful, and think about user adoption | ||
| from the very beginning." | ||
| summary: "We create, maintain, and develop Shiny applications | ||
| for enterprise customers all over the world. Appsilon | ||
| provides scalability, security, and modern UI/UX with | ||
| custom R packages that native Shiny apps do not provide. | ||
| Our team is among the world's foremost experts in R Shiny | ||
| and has made a variety of Shiny innovations over the | ||
| years. Appsilon is a proud Posit Full Service | ||
| Certified Partner." | ||
| footer: | ||
| text: "Designed and developed with 💙 by" | ||
| link: | ||
| label: "Appsilon" | ||
| url: "https://go.appsilon.com/appsilon-user-metrics-app" | ||
|
|
||
| logo: "appsilon-logo.png" | ||
|
|
||
| color: | ||
| palette: | ||
| white: "#FFFFFF" | ||
| mint: "#00CDA3" | ||
| blue: "#0099F9" | ||
| yellow: "#E8C329" | ||
| purple: "#994B9D" | ||
| black: "#000000" | ||
| gray: "#15354A" | ||
| foreground: gray | ||
| background: white | ||
| primary: blue | ||
|
|
||
| typography: | ||
| fonts: | ||
| - family: Maven Pro | ||
| source: google | ||
| weight: [400, 500, 600, 700] | ||
| style: normal | ||
| - family: Roboto | ||
| source: google | ||
| weight: [400, 500, 600] | ||
| style: normal | ||
| base: | ||
| Roboto | ||
| headings: | ||
| Maven Pro |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,2 @@ | ||
| # Rhino / shinyApp entrypoint. Do not edit. | ||
| rhino::app() |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| static_data.rds |
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I noticed there are some features not included in the It makes me wonder if there should be a CONTRIBUTING guide here that includes some of those details so this file, for example, is a bit easier to understand. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This app here is a simplified version of the origina Connect User Metrics; some of the features only make sense in the complete app, where the user is able to modify some configuration files. I will add a CONTRIBUTE file pointing to the original branch. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,2 @@ | ||
| users: | ||
| - this_user_will_not_appear_on_the_app |
|
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,2 @@ | ||
| # Logic: application code independent from Shiny. | ||
| # https://go.appsilon.com/rhino-project-structure |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,130 @@ | ||
| box::use( | ||
| dplyr, | ||
| lubridate[floor_date], | ||
| magrittr[`%>%`], | ||
| stats[setNames], | ||
| tools[toTitleCase], | ||
| ) | ||
|
|
||
| box::use( | ||
| app/logic/utils[ | ||
| week_start_day, | ||
| week_start_id | ||
| ], | ||
| ) | ||
|
|
||
| #' Aggregate usage data based on specified levels and time period | ||
| #' @param usage Usage data frame | ||
| #' @param agg_levels Aggregation levels | ||
| #' @param date_aggregation Time period for aggregation | ||
| #' @return Aggregated data frame | ||
| aggregate_usage <- function(usage, agg_levels, date_aggregation) { | ||
| usage$day <- floor_date(usage$start_date, "day") | ||
| usage$week <- floor_date(usage$start_date, "week", | ||
| week_start = week_start_id | ||
| ) | ||
| usage$month <- floor_date(usage$start_date, "month") | ||
|
|
||
| if (date_aggregation == "day") { | ||
| usage$start_date <- usage$start_date | ||
| } else if (date_aggregation == "week") { | ||
| usage$start_date <- usage$week | ||
| } else if (date_aggregation == "month") { | ||
| usage$start_date <- usage$month | ||
| } | ||
|
|
||
| if (!is.null(agg_levels)) { | ||
| usage <- usage %>% | ||
| dplyr$group_by(dplyr$across(dplyr$all_of(agg_levels))) | ||
| } | ||
|
|
||
| user_in_agg_levels <- "user_guid" %in% agg_levels | ||
| if (user_in_agg_levels) { | ||
| usage <- usage %>% | ||
| dplyr$filter(!is.na(user_guid)) | ||
| } | ||
|
|
||
| usage %>% | ||
| dplyr$summarise( | ||
| avg_duration = mean(duration, na.rm = TRUE), | ||
| "Session count" = dplyr$n(), | ||
| "Unique users" = dplyr$n_distinct(user_guid) | ||
| ) %>% | ||
| dplyr$ungroup() | ||
| } | ||
|
|
||
| #' Add metadata to aggregated usage data | ||
| #' @param agg_usage Aggregated usage data | ||
| #' @param apps Apps data frame | ||
| #' @param users Users data frame | ||
| #' @param content_guid_present Whether content_guid is in aggregation levels | ||
| #' @param user_guid_present Whether user_guid is in aggregation levels | ||
| #' @return Aggregated usage data with metadata | ||
| add_metadata <- function(agg_usage, apps, users, content_guid_present, user_guid_present) { | ||
| if (content_guid_present) { | ||
| agg_usage <- agg_usage %>% | ||
| dplyr$left_join(apps, by = c("content_guid" = "guid")) | ||
| } | ||
| if (user_guid_present) { | ||
| agg_usage <- agg_usage %>% | ||
| dplyr$left_join(users, by = c("user_guid" = "guid")) | ||
| } | ||
| agg_usage | ||
| } | ||
|
|
||
| #' Process aggregated usage data | ||
| #' @param usage Usage data frame | ||
| #' @param agg_levels Vector of aggregation levels | ||
| #' @param date_aggregation Date aggregation level | ||
| #' @param apps Apps data frame | ||
| #' @param users Users data frame | ||
| #' @return Aggregated usage data frame | ||
| #' @export | ||
| process_agg_usage <- function(usage, agg_levels, date_aggregation, apps, users) { | ||
| content_guid_present <- "content_guid" %in% agg_levels | ||
| user_guid_present <- "user_guid" %in% agg_levels | ||
|
|
||
| # If neither content_guid nor user_guid is present, just do basic aggregation | ||
| if (!content_guid_present && !user_guid_present) { | ||
| return(aggregate_usage(usage, agg_levels, date_aggregation)) | ||
| } | ||
|
|
||
| # If no start_date in agg_levels and only one of content/user guid, | ||
| # force both to be present | ||
| if (!"start_date" %in% agg_levels && xor(content_guid_present, user_guid_present)) { | ||
| agg_levels <- c("content_guid", "user_guid") | ||
| content_guid_present <- user_guid_present <- TRUE | ||
| } | ||
|
|
||
| # Do the aggregation and add metadata | ||
| agg_usage <- aggregate_usage(usage, agg_levels, date_aggregation) | ||
| add_metadata(agg_usage, apps, users, content_guid_present, user_guid_present) | ||
| } | ||
|
|
||
| #' Format aggregated usage data for display | ||
| #' @param agg_usage Aggregated usage data frame | ||
| #' @param date_aggregation Date aggregation level ("week", "month", or "day") | ||
| #' @param format_duration Function to format duration values | ||
| #' @return Formatted data frame for display | ||
| #' @export | ||
| format_agg_usage <- function(agg_usage, date_aggregation, format_duration) { | ||
| date_col <- switch(date_aggregation, | ||
| "week" = paste(toTitleCase(week_start_day), "Date"), | ||
| "month" = "Month", | ||
| "Date" | ||
| ) | ||
|
|
||
| # Create ordered column mapping | ||
| cols <- c( | ||
| setNames("title", "Application"), | ||
| setNames("username", "Username"), | ||
| setNames("start_date", date_col), | ||
| "Session count", | ||
| "Unique users", | ||
| setNames("avg_duration", "Average session duration") | ||
| ) | ||
|
|
||
| agg_usage %>% | ||
| dplyr$mutate(avg_duration = format_duration(avg_duration)) %>% | ||
| dplyr$select(dplyr$any_of(cols)) | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Great README. I noticed you included a short description in the
manifest.json, but it could be helpful to add a description here as well. Perhaps one even longer that gives an indication of what functionality to expect.Additionally I see the "Connect Insights Dashboard" naming here, but the directory and
manifest.jsonuse "Connect User Metrics" I was curious about the difference in naming.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Excellent idea! I will copy the content of the in-app "help button" here, so we have a better description of what the app really does.
About the naming convention: you are right, we will stick to "Connect User Metrics".