Skip to content

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

Open
wants to merge 24 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
aac0e77
add connect-user-metrics dir
Jun 6, 2025
ad72f9d
add manifest file
Jun 6, 2025
6820cb0
add connect-user-metrics to enxtensions.yml
Jun 6, 2025
627dabf
shorter readme
Jun 9, 2025
93a2a8a
use Posit package manager
Jun 9, 2025
94e6b43
add dot-files
Jun 10, 2025
e8baeaa
Merge remote-tracking branch 'origin/copy-user-metrics' into user-met…
Jun 10, 2025
6ac0883
use posit package manager
Jun 10, 2025
7107381
remove goal lines from config.yml
Jun 10, 2025
42436cf
indent manifest.json
Jun 10, 2025
4572c0f
Merge pull request #5 from Appsilon/copy-user-metrics
vituri Jun 10, 2025
8ba0141
fix homepage link
Jun 10, 2025
4d0401a
remove tags; update description
Jun 11, 2025
d9d70f7
Merge branch 'user-metrics-prepare-manifest-files' into connect-user-…
Jun 12, 2025
a1861d1
update links
Jun 12, 2025
18ee46d
Merge pull request #6 from Appsilon/use-shortener-links
vituri Jun 12, 2025
18e8d93
Add About the app section
jakubnowicki Jun 17, 2025
e772ebc
Merge pull request #9 from Appsilon/update-modal-text
jakubnowicki Jun 17, 2025
6133051
Pre-release manifest.json update.
jakubnowicki Jun 17, 2025
2a282bb
Merge pull request #10 from Appsilon/pre-release-adjustments
jakubnowicki Jun 17, 2025
499ba46
copy changes from main-connect-extension
Jun 24, 2025
be09423
copy changes from main-connect-extension
Jun 24, 2025
bba6a4f
Merge remote-tracking branch 'refs/remotes/origin/pre-connect-release…
Jun 24, 2025
f31a566
Merge pull request #14 from Appsilon/pre-connect-release-fixes
vituri Jun 24, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/workflows/extensions.yml
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ jobs:
usage-metrics-dashboard: extensions/usage-metrics-dashboard/**
voila-example: extensions/voila-example/**
stock-report: extensions/stock-report/**
connect-user-metrics: extensions/connect-user-metrics/**

# Runs for each extension that has changed from `simple-extension-changes`
# Lints and packages in preparation for tests and and release.
Expand Down
19 changes: 19 additions & 0 deletions extensions/connect-user-metrics/.Rprofile
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"))
4 changes: 4 additions & 0 deletions extensions/connect-user-metrics/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
.Renviron
.Rproj.user
.Rhistory
.DS_Store
5 changes: 5 additions & 0 deletions extensions/connect-user-metrics/.lintr
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)
)
3 changes: 3 additions & 0 deletions extensions/connect-user-metrics/.renvignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# Only use `dependencies.R` to infer project dependencies.
*
!dependencies.R
7 changes: 7 additions & 0 deletions extensions/connect-user-metrics/.rscignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
.github
.lintr
.renvignore
.Renviron
.rhino
.rscignore
tests
5 changes: 5 additions & 0 deletions extensions/connect-user-metrics/CONTRIBUTE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# How to contribute

This branch is a *simplified* version of Connect User Metrics, tailored to be deployed on Posit Connect Gallery.

For the full version of the app, please refer to [the main branch of Connect User Metrics](https://github.com/Appsilon/ConnectUserMetrics).
66 changes: 66 additions & 0 deletions extensions/connect-user-metrics/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
# Connect User Metrics Dashboard

Connect User Metrics makes it easy to monitor **application adoption**,
track **user engagement** and access detailed **usage analytics**
for all your Shiny applications deployed on Posit Connect.
Some key features:

- **Time-based analysis**: View data by day, week, or month across custom time periods
- **Flexible grouping**: Combine metrics by application, user, and date for different perspectives
- **Interactive charts**: Visualize session counts and unique users with dynamic filtering
- **Smart filtering**: Set minimum session duration and filter by specific apps or users
- **Data export**: Download raw and aggregated data as CSV files for further analysis

## 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.co/connect/user/content-settings/#content-vars
[rsconnect-auth]: https://go.appsilon.com/why-use-rstudio-connect-authentication-user-metrics-app
[Configuration appendix]: https://docs.posit.co/connect/admin/appendix/configuration/
[DefaultServerEnv]: https://docs.posit.co/connect/admin/appendix/configuration/#Applications.DefaultServerEnv
[DefaultAPIKeyEnv]: https://docs.posit.co/connect/admin/appendix/configuration/#Applications.DefaultAPIKeyEnv
[Instrumentation]: https://docs.posit.co/connect/admin/appendix/configuration/#Metrics.Instrumentation
63 changes: 63 additions & 0 deletions extensions/connect-user-metrics/_brand.yml
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
2 changes: 2 additions & 0 deletions extensions/connect-user-metrics/app.R
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
# Rhino / shinyApp entrypoint. Do not edit.
rhino::app()
1 change: 1 addition & 0 deletions extensions/connect-user-metrics/app/constants/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
static_data.rds
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
users:
- this_user_will_not_appear_on_the_app
2 changes: 2 additions & 0 deletions extensions/connect-user-metrics/app/logic/__init__.R
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
130 changes: 130 additions & 0 deletions extensions/connect-user-metrics/app/logic/aggregation.R
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))
}
Loading