Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
15 changes: 14 additions & 1 deletion .github/workflows/extensions.yml
Original file line number Diff line number Diff line change
Expand Up @@ -153,12 +153,14 @@ jobs:
permissions:
pull-requests: read
outputs:
publisher-command-center-otel: ${{ steps.changes.outputs.publisher-command-center-otel }}
# Adding a new extension with a complex build process?
# Add a new line here with the name of the extension as the name of the
# filter and the step output variable below
# e.g. `extension-name: ${{ steps.changes.outputs.extension-name }}`
# Be sure the extension name and directory name it is in are the same
publisher-command-center-otel: ${{ steps.changes.outputs.publisher-command-center-otel }}
usage-metrics-dashboard-otel: ${{ steps.changes.outputs.usage-metrics-dashboard-otel }}


steps:
- uses: actions/checkout@v4
Expand All @@ -172,6 +174,16 @@ jobs:
# # e.g. `extension-name: extensions/extension-name/**`
filters: |
publisher-command-center-otel: extensions/publisher-command-center-otel/**
usage-metrics-dashboard-otel: extensions/usage-metrics-dashboard-otel/**

# Creates and releases the Usage Metrics Dashboard extension using a custom workflow
usage-metrics-dashboard-otel:
needs: [complex-extension-changes]
# Only runs if the `complex-extension-changes` job detects changes in the
# usage-metrics-dashboard-otel extension directory
if: ${{ needs.complex-extension-changes.outputs.usage-metrics-dashboard-otel == 'true' }}
uses: ./.github/workflows/usage-metrics-dashboard-otel.yml
secrets: inherit

# Creates and releases the Publisher Command Center extension using a custom
# workflow
Expand All @@ -196,6 +208,7 @@ jobs:
needs: [
simple-extension-release,
publisher-command-center-otel,
usage-metrics-dashboard-otel,
# Add complex extension job names here as needed
]
if: ${{ always() }}
Expand Down
99 changes: 99 additions & 0 deletions .github/workflows/usage-metrics-dashboard-otel.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
name: Usage Metrics Dashboard

# Re-usable workflows use the `workflow_call` trigger
# https://docs.github.com/en/actions/sharing-automations/reusing-workflows#creating-a-reusable-workflow
on:
workflow_call:

# Setup the environment with the extension name for easy re-use
# Also set the GH_TOKEN for the release-extension action to be able to use gh
env:
EXTENSION_NAME: usage-metrics-dashboard-otel
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}

jobs:
extension:
runs-on: ubuntu-latest
defaults:
run:
working-directory: ./extensions/${{ env.EXTENSION_NAME }}

steps:
# Checkout the repository so the rest of the actions can run with no issue
- uses: actions/checkout@v4

# We want to fail quickly if the linting fails, do that first
- uses: ./.github/actions/lint-extension
with:
extension-name: ${{ env.EXTENSION_NAME }}

# ---
# Run R tests
# ---

- uses: r-lib/actions/setup-r@v2
with:
r-version: '4.3.3'

- name: Install system dependencies
run: |
sudo apt-get update
sudo apt-get install -y \
protobuf-compiler \
libprotobuf-dev \
libcurl4-openssl-dev \
pkg-config

- uses: r-lib/actions/setup-renv@v2
with:
working-directory: extensions/${{ env.EXTENSION_NAME }}

- run: Rscript -e 'install.packages(c("testthat"))'
working-directory: extensions/${{ env.EXTENSION_NAME }}

- run: Rscript -e 'testthat::test_local()'
working-directory: extensions/${{ env.EXTENSION_NAME }}

# Now that the extension is built we need to upload an artifact to pass
# to the package-extension action that contains the files we want to be
# included in the extension
# This only includes necessary files for the extension to run leaving out
# the files that were used to build the /dist/ directory
- name: Upload built extension
uses: actions/upload-artifact@v4
with:
name: ${{ env.EXTENSION_NAME }}
# Replace the below with the files your content needs
path: |
extensions/${{ env.EXTENSION_NAME }}/app.R
extensions/${{ env.EXTENSION_NAME }}/R/
extensions/${{ env.EXTENSION_NAME }}/renv.lock
extensions/${{ env.EXTENSION_NAME }}/www/styles.css
extensions/${{ env.EXTENSION_NAME }}/manifest.json

# Package up the extension into a TAR using the package-extension action
- uses: ./.github/actions/package-extension
with:
extension-name: ${{ env.EXTENSION_NAME }}
artifact-name: ${{ env.EXTENSION_NAME }}

connect-integration-tests:
needs: extension
uses: ./.github/workflows/connect-integration-tests.yml
secrets: inherit
with:
extensions: '["usage-metrics-dashboard-otel"]' # JSON array format to match the workflow input schema

release:
runs-on: ubuntu-latest
needs: [extension, connect-integration-tests]
# Release the extension using the release-extension action
# Will only create a GitHub release if merged to `main` and the semver
# version has been updated
steps:
# Checkout the repository so the rest of the actions can run with no issue
- uses: actions/checkout@v4

- uses: ./.github/actions/release-extension
with:
extension-name: ${{ env.EXTENSION_NAME }}
2 changes: 2 additions & 0 deletions extensions/usage-metrics-dashboard-otel/.Rbuildignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
^renv$
^renv\.lock$
1 change: 1 addition & 0 deletions extensions/usage-metrics-dashboard-otel/.Rprofile
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
source("renv/activate.R")
3 changes: 3 additions & 0 deletions extensions/usage-metrics-dashboard-otel/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
.posit/
app_cache/
.Renviron
2 changes: 2 additions & 0 deletions extensions/usage-metrics-dashboard-otel/.renvignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
tests
DESCRIPTION
13 changes: 13 additions & 0 deletions extensions/usage-metrics-dashboard-otel/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# Changelog

All notable changes to the Usage Metrics Dashboard (with OTel Tracing) extension will be documented in this file.

The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [1.0.0]

### Added

- Forked from Usage Metrics Dashboard v1.0.8
- Added first pass of OTel tracing.
11 changes: 11 additions & 0 deletions extensions/usage-metrics-dashboard-otel/CONTRIBUTING.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# Contributing to Usage Metrics Dashboard

## Organization

- The app's core code lives in `app.R`.
- Supporting functions live in `R/`.

## Tests

- Run `make test` to run tests.
- Tests live in `tests/testthat`, and run against the code in `R/`.
2 changes: 2 additions & 0 deletions extensions/usage-metrics-dashboard-otel/DESCRIPTION
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Package: usage-metrics-dashboard
Version: 1.0.9
21 changes: 21 additions & 0 deletions extensions/usage-metrics-dashboard-otel/Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
.PHONY: test update-manifest

test:
Rscript -e 'testthat::test_local()'

# This recipe updates the manifest, with a few helpful modifications:
# - Copies over `extension` and `environment` blocks from the old manifest to
# the new one. These are Gallery-specific blocks, and not created by
# `rsconnect`.
# - Preserves set of files listed in the `files` block, where
# `rsconnect::writeManifest()` by default includes all the files in the
# directory.
update-manifest:
cp manifest.json manifest.old.json
FILES=$$(jq -r '.files | keys | join(",")' manifest.old.json); \
Rscript -e "rsconnect::writeManifest(appFiles = strsplit('$$FILES', ',')[[1]])"; \
jq -n --slurpfile old manifest.old.json --slurpfile new manifest.json \
'$$new[0] * {"environment": $$old[0].environment, "extension": $$old[0].extension}' \
> manifest.merged.json
mv manifest.merged.json manifest.json
rm manifest.old.json
84 changes: 84 additions & 0 deletions extensions/usage-metrics-dashboard-otel/R/get_content_vendored.R
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
get_content_stock_vendored <- function(
src,
guid = NULL,
owner_guid = NULL,
name = NULL,
...,
.p = NULL
) {
connectapi:::validate_R6_class(src, "Connect")
if (connectapi:::compare_connect_version(src$version, "2024.06.0") < 0) {
include <- "tags,owner"
content_ptype <- connectapi:::connectapi_ptypes$content[,
names(connectapi:::connectapi_ptypes$content) != "vanity_url"
]
} else {
include <- "tags,owner,vanity_url"
content_ptype <- connectapi:::connectapi_ptypes$content
}
res <- src$content(
guid = guid,
owner_guid = owner_guid,
name = name,
include = include
)
if (!is.null(guid)) {
res <- list(res)
}
if (!is.null(.p)) {
res <- res %>% purrr::keep(.p = .p)
}
out <- connectapi:::parse_connectapi_typed(res, content_ptype)
return(out)
}

get_content_noparse <- function(
src,
guid = NULL,
owner_guid = NULL,
name = NULL,
...,
.p = NULL
) {
connectapi:::validate_R6_class(src, "Connect")
if (connectapi:::compare_connect_version(src$version, "2024.06.0") < 0) {
include <- "tags,owner"
content_ptype <- connectapi:::connectapi_ptypes$content[,
names(connectapi:::connectapi_ptypes$content) != "vanity_url"
]
} else {
include <- "tags,owner,vanity_url"
content_ptype <- connectapi:::connectapi_ptypes$content
}

# Also vendor the client's content function, just to add parser = NULL to the args.
src_content <- function(
guid = NULL,
owner_guid = NULL,
name = NULL,
include = "tags,owner"
) {
if (!is.null(guid)) {
return(src$GET(connectapi:::v1_url("content", guid), query = list(include = include)))
}
query <- list(owner_guid = owner_guid, name = name, include = include)
path <- connectapi:::v1_url("content")
src$GET(path, query = query, parser = NULL)
}

res <- src_content(
guid = guid,
owner_guid = owner_guid,
name = name,
include = "owner"
)
if (!is.null(guid)) {
res <- list(res)
}
if (!is.null(.p)) {
res <- res %>% purrr::keep(.p = .p)
}
res_text <- httr::content(res, as = "text")
jsonlite::fromJSON(res_text, flatten = TRUE)
}

56 changes: 56 additions & 0 deletions extensions/usage-metrics-dashboard-otel/R/integrations.R
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
library(connectapi)
library(purrr)
library(dplyr)

get_eligible_integrations <- function(client) {
tryCatch(
{
# TODO When https://github.com/posit-dev/connectapi/issues/413 is closed,
# remove this and use that functionality instead.
integrations <- client$GET("v1/oauth/integrations")

integrations_df <- map_dfr(integrations, function(record) {
# Extract main fields
main_fields <- discard(record, is.list) # Discard list fields like 'config'

# Extract and combine the config fields with field names and values
config <- paste(
imap_chr(record$config, ~ paste(.y, .x, sep = ": ")),
collapse = ", "
)

# Combine both into a single list
c(main_fields, config = config)
})

print(integrations_df)

eligible_integrations <- integrations_df |>
filter(
template == "connect",
config %in% c("max_role: Admin", "max_role: Publisher")
)
},
error = function(e) {
data.frame()
}
)
}

auto_add_integration <- function(client, integration_guid) {
print("About to PUT the integration!")

# TODO When https://github.com/posit-dev/connectapi/issues/414 is implemented,
# delete this and use that instead.
client$PUT(
connectapi:::v1_url(
"content",
Sys.getenv("CONNECT_CONTENT_GUID"),
"oauth",
"integrations",
"associations"
),
body = list(list(oauth_integration_guid = integration_guid))
)
print("Done adding the integration")
}
30 changes: 30 additions & 0 deletions extensions/usage-metrics-dashboard-otel/R/ui_components.R
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
library(shiny)

# Create a bar chart HTML element for use in the content reactable.
bar_chart <- function(
value,
max_val,
height = "1rem",
fill = "#7494b1",
background = NULL
) {
width <- paste0(value * 100 / max_val, "%")
value <- format(value, width = nchar(max_val), justify = "right")
bar <- div(class = "bar", style = list(background = fill, width = width))
chart <- div(class = "bar-chart", style = list(background = background), bar)
label <- span(class = "number", value)
div(class = "bar-cell", label, chart)
}

# Construct full URL from Shiny session
full_url <- function(session) {
paste0(
session$clientData$url_protocol,
"//",
session$clientData$url_hostname,
if (nzchar(session$clientData$url_port)) {
paste0(":", session$clientData$url_port)
},
session$clientData$url_pathname
)
}
20 changes: 20 additions & 0 deletions extensions/usage-metrics-dashboard-otel/R/visit_processing.R
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
library(dplyr)
library(lubridate)
library(tidyr)

# Removes visits that are within a specified time window of the previous visit
# for the same user and content.
filter_visits_by_time_window <- function(visits, session_window) {
if (session_window == 0) {
return(visits)
} else {
visits |>
group_by(content_guid, user_guid) |>
# Compute time diffs and filter out hits within the session
mutate(time_diff = seconds(timestamp - lag(timestamp, 1))) |>
replace_na(list(time_diff = seconds(Inf))) |>
filter(time_diff > session_window) |>
ungroup() |>
select(-time_diff)
}
}
Loading
Loading