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
22 changes: 12 additions & 10 deletions extensions/content-health-monitor/content-health-monitor.qmd
Original file line number Diff line number Diff line change
Expand Up @@ -32,15 +32,6 @@ current_user_name = utils.DEFAULT_USER_NAME # Will be updated if user info can
# Read environment variables
connect_server = utils.get_env_var("CONNECT_SERVER", state) # Automatically provided by Connect, must be set when previewing locally
api_key = utils.get_env_var("CONNECT_API_KEY", state) # Automatically provided by Connect, must be set when previewing locally
monitored_content_guid = utils.get_env_var("MONITORED_CONTENT", state)

# Extract GUID if it's a string or URL containing a GUID
if monitored_content_guid:
monitored_content_guid, guid_error_message = utils.extract_guid(monitored_content_guid)
# Handle URL with no GUID error
if guid_error_message:
state.show_instructions = True
state.instructions.append(guid_error_message)

# Check if we have the required environment variables to instantiate the client
client = None
Expand All @@ -50,7 +41,7 @@ if has_connect_env_vars:
try:
# Instantiate a Connect client using posit-sdk
client = connect.Client()

# Get current user's full name - function handles errors internally
user_name = utils.get_current_user_full_name(client)
if user_name != "Unknown": # Only update if we got a valid name
Expand All @@ -60,6 +51,17 @@ if has_connect_env_vars:
state.show_instructions = True
state.instructions.append(f"<b>Error initializing Connect client:</b> {str(e)}")

# Read MONITORED_CONTENT after client is created so version detection can pick the right instructions
monitored_content_guid = utils.get_env_var("MONITORED_CONTENT", state, client=client)

# Extract GUID if it's a string or URL containing a GUID
if monitored_content_guid:
monitored_content_guid, guid_error_message = utils.extract_guid(monitored_content_guid)
# Handle URL with no GUID error
if guid_error_message:
state.show_instructions = True
state.instructions.append(guid_error_message)


# ------ DATA GATHERING SECTION ------ #
# Always record the current time for reporting purposes
Expand Down
112 changes: 81 additions & 31 deletions extensions/content-health-monitor/content_health_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,20 @@
import os
import re
import requests
from packaging.version import Version
from posit import connect

NEW_SETTINGS_UI_VERSION = Version("2026.03.0")


def has_new_settings_ui(client):
try:
if client.version:
return Version(client.version) >= NEW_SETTINGS_UI_VERSION
except Exception:
pass
return True

class MonitorState:
"""State container for content health monitor"""

Expand Down Expand Up @@ -56,52 +68,90 @@ def __init__(self):
CSS_GRID_STYLE = "display: grid; grid-template-columns: 150px auto; grid-gap: 8px; padding: 10px 0;"

# Helper function to read environment variables and add instructions if missing
def get_env_var(var_name, state, description=""):
def get_env_var(var_name, state, description="", client=None):
"""Get environment variable and add instruction if missing"""
value = os.environ.get(var_name, "")
if not value:
state.show_instructions = True

# Generic instruction for most variables
if var_name != "MONITORED_CONTENT":
instruction = f"Please set the <code>{var_name}</code> environment variable."
# Detailed instructions for MONITORED_CONTENT
else:
one_tab = "&nbsp;&nbsp;&nbsp;&nbsp;" # For indentation in HTML
instruction = (
f"To monitor a piece of content you must configure the <code>{var_name}</code> environment variable.<br><br>"

f"<h2>Step 1: Find the content to be monitored</h2>"
f"{one_tab}• In a separate tab, open the content you wish to monitor<br>"
f"{one_tab}• In the browser address bar, copy the entire address (URL)<br><br>"
f"{one_tab}<img src=\"images/address-bar.png\" alt=\"Browser address bar\" style=\"max-width: 50%; margin: 10px 0 10px 0; border: 1px solid #ddd;\"><br>"

f"<h2>Step 2: Configure this report</h2>"
f"{one_tab}• Return to this report<br>"
f"{one_tab}• Click the <b>gear icon</b> at the top right of the screen to open <b>Content Settings</b><br>"
f"{one_tab}<img src=\"images/settings-gear-icon.png\" alt=\"Content Settings button\" style=\"max-width: 30%; margin: 10px 0 10px 0; border: 1px solid #ddd;\"><br>"
f"{one_tab}• Click the <b>Vars</b> tab<br>"
f"{one_tab}• In the <b>Name</b> field enter <code>{var_name}</code><br>"
f"{one_tab}• In the <b>Value</b> field paste the full address you copied in the previous step<br>"
f"{one_tab}• It should look like the example below<br>"
f"{one_tab}<img src=\"images/vars.png\" alt=\"Environment Variables tab\" style=\"max-width: 30%; margin: 10px 0 10px 0; border: 1px solid #ddd;\"><br>"
f"{one_tab}• Click <b>Add Variable</b><br>"
f"{one_tab}• Click <b>Save</b> at the top of the screen to save your changes<br><br>"

f"<h2>Step 3: Run the report to execute the health check</h2>"
f"{one_tab}• Click the <b>Refresh Report</b> button at the top right to run a health check against the monitored content<br>"
f"{one_tab}• The health check will run and report the status<br>"
f"{one_tab}• If the content is healthy, you will see a <b>PASS</b> status, otherwise you will see a <b>FAIL</b> status<br>"
f"{one_tab}• If you see a <b>FAIL</b> status, click the <b>View Logs</b> link to see more details about the failure<br>"
f"{one_tab}<img src=\"images/refresh-report.png\" alt=\"Refresh report button\" style=\"max-width: 30%; margin: 10px 0 10px 0; border: 1px solid #ddd;\"><br><br>"

)

use_new_ui = has_new_settings_ui(client) if client else True

if use_new_ui:
instruction = _build_new_ui_instructions(var_name, one_tab)
else:
instruction = _build_old_ui_instructions(var_name, one_tab)

if description:
instruction += f" {description}"
state.instructions.append(instruction)
return value


def _build_new_ui_instructions(var_name, one_tab):
return (
f"To monitor a piece of content you must configure the <code>{var_name}</code> environment variable.<br><br>"

f"<h2>Step 1: Find the content to be monitored</h2>"
f"{one_tab}• In a separate tab, open the content you wish to monitor<br>"
f"{one_tab}• In the browser address bar, copy the entire address (URL)<br><br>"
f"{one_tab}<img src=\"images/address-bar.png\" alt=\"Browser address bar\" style=\"max-width: 50%; margin: 10px 0 10px 0; border: 1px solid #ddd;\"><br>"

f"<h2>Step 2: Configure this report</h2>"
f"{one_tab}• Return to this report<br>"
f"{one_tab}• Click <b>Settings</b> at the top right of the screen<br>"
f"{one_tab}<img src=\"images/settings-button.png\" alt=\"Settings button in toolbar\" style=\"max-width: 30%; margin: 10px 0 10px 0; border: 1px solid #ddd;\"><br>"
f"{one_tab}• Click the <b>Advanced</b> tab<br>"
f"{one_tab}• In the <b>Name</b> field enter <code>{var_name}</code><br>"
f"{one_tab}• In the <b>Value</b> field paste the full address you copied in the previous step<br>"
f"{one_tab}• It should look like the example below<br>"
f"{one_tab}<img src=\"images/env-vars.png\" alt=\"Environment Variables section in Advanced tab\" style=\"max-width: 30%; margin: 10px 0 10px 0; border: 1px solid #ddd;\"><br>"
f"{one_tab}• Click <b>+ Add</b><br>"
f"{one_tab}• Click <b>Save</b> to save your changes<br><br>"

f"<h2>Step 3: Run the report to execute the health check</h2>"
f"{one_tab}• Click the <b>Refresh</b> button at the top right to run a health check against the monitored content<br>"
f"{one_tab}<img src=\"images/refresh-report.png\" alt=\"Refresh button in toolbar\" style=\"max-width: 30%; margin: 10px 0 10px 0; border: 1px solid #ddd;\"><br>"
f"{one_tab}• The health check will run and report the status<br>"
f"{one_tab}• If the content is healthy, you will see a <b>PASS</b> status, otherwise you will see a <b>FAIL</b> status<br>"
f"{one_tab}• If you see a <b>FAIL</b> status, click <b>Logs</b> to see more details about the failure<br><br>"
)


def _build_old_ui_instructions(var_name, one_tab):
return (
f"To monitor a piece of content you must configure the <code>{var_name}</code> environment variable.<br><br>"

f"<h2>Step 1: Find the content to be monitored</h2>"
f"{one_tab}• In a separate tab, open the content you wish to monitor<br>"
f"{one_tab}• In the browser address bar, copy the entire address (URL)<br><br>"
f"{one_tab}<img src=\"images/address-bar.png\" alt=\"Browser address bar\" style=\"max-width: 50%; margin: 10px 0 10px 0; border: 1px solid #ddd;\"><br>"

f"<h2>Step 2: Configure this report</h2>"
f"{one_tab}• Return to this report<br>"
f"{one_tab}• Click the <b>gear icon</b> at the top right of the screen to open <b>Content Settings</b><br>"
f"{one_tab}<img src=\"images/settings-gear-icon.png\" alt=\"Content Settings button\" style=\"max-width: 30%; margin: 10px 0 10px 0; border: 1px solid #ddd;\"><br>"
f"{one_tab}• Click the <b>Vars</b> tab<br>"
f"{one_tab}• In the <b>Name</b> field enter <code>{var_name}</code><br>"
f"{one_tab}• In the <b>Value</b> field paste the full address you copied in the previous step<br>"
f"{one_tab}• It should look like the example below<br>"
f"{one_tab}<img src=\"images/vars.png\" alt=\"Environment Variables tab\" style=\"max-width: 30%; margin: 10px 0 10px 0; border: 1px solid #ddd;\"><br>"
f"{one_tab}• Click <b>Add Variable</b><br>"
f"{one_tab}• Click <b>Save</b> at the top of the screen to save your changes<br><br>"

f"<h2>Step 3: Run the report to execute the health check</h2>"
f"{one_tab}• Click the <b>Refresh Report</b> button at the top right to run a health check against the monitored content<br>"
f"{one_tab}• The health check will run and report the status<br>"
f"{one_tab}• If the content is healthy, you will see a <b>PASS</b> status, otherwise you will see a <b>FAIL</b> status<br>"
f"{one_tab}• If you see a <b>FAIL</b> status, click the <b>View Logs</b> link to see more details about the failure<br>"
f"{one_tab}<img src=\"images/refresh-report.png\" alt=\"Refresh report button\" style=\"max-width: 30%; margin: 10px 0 10px 0; border: 1px solid #ddd;\"><br><br>"
)

# Helper function to extract error messages from exceptions
def format_error_message(exception):
"""Extract a clean error message from various exception types"""
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified extensions/content-health-monitor/images/refresh-report.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
18 changes: 12 additions & 6 deletions extensions/content-health-monitor/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
"category": "extension",
"tags": [],
"minimumConnectVersion": "2025.04.0",
"version": "1.0.0"
"version": "1.0.1"
},
"environment": {
"python": {
Expand Down Expand Up @@ -41,22 +41,28 @@
"checksum": "5f89d52674b219c0b0ed85f1a5785641"
},
"content-health-monitor.qmd": {
"checksum": "d108f54df865a66e2ac54a7fb3ac9772"
"checksum": "8b858ac8a1f4515e17bb1ee40feff087"
},
"content_health_utils.py": {
"checksum": "5b7c8db239c4b9e0782060b1172b2456"
"checksum": "44fc4d0d3089dca3e1aed8663758f173"
},
"images/vars.png": {
"checksum": "9840b32e4adec7d5a3c70bb3373a63bb"
"images/settings-button.png": {
"checksum": "4e79b7f99629d48bf1170a8d6a2fdc20"
},
"images/env-vars.png": {
"checksum": "4e9fe7a130a658dbdd0bf3194e04c73c"
},
"images/settings-gear-icon.png": {
"checksum": "e2bc43263eb8a9179b6e0e5ab3e22450"
},
"images/vars.png": {
"checksum": "9840b32e4adec7d5a3c70bb3373a63bb"
},
"images/address-bar.png": {
"checksum": "993cc8f97996c68f30527abbcc63cf3c"
},
"images/refresh-report.png": {
"checksum": "e5680e6188eb8d659e4313cb89d0be3b"
"checksum": "8216a71e864ec60ef8bbddf921f8331f"
}
}
}
95 changes: 95 additions & 0 deletions extensions/content-health-monitor/test_content_health_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
get_env_var = content_health_utils.get_env_var
get_user = content_health_utils.get_user
has_error = content_health_utils.has_error
has_new_settings_ui = content_health_utils.has_new_settings_ui
should_send_email = content_health_utils.should_send_email
validate = content_health_utils.validate

Expand Down Expand Up @@ -1106,3 +1107,97 @@ def test_check_server_reachable_http_error(self, connect_test_server, api_test_k
assert "500 Server Error" in str(excinfo.value)
mock_get.assert_called_once()


class TestHasNewSettingsUi:

def test_new_version_returns_true(self):
"""Connect >= 2026.03.0 should use new UI instructions"""
client = MagicMock()
client.version = "2026.03.0"
assert has_new_settings_ui(client) is True

def test_newer_version_returns_true(self):
"""Connect versions after 2026.03.0 should use new UI instructions"""
client = MagicMock()
client.version = "2026.05.0"
assert has_new_settings_ui(client) is True

def test_old_version_returns_false(self):
"""Connect < 2026.03.0 should use old UI instructions"""
client = MagicMock()
client.version = "2025.04.0"
assert has_new_settings_ui(client) is False

def test_preview_version_returns_false(self):
"""Connect 2026.02.x (new UI opt-in only) should use old UI instructions"""
client = MagicMock()
client.version = "2026.02.0"
assert has_new_settings_ui(client) is False

def test_none_version_defaults_to_true(self):
"""When version is None, default to new UI instructions"""
client = MagicMock()
client.version = None
assert has_new_settings_ui(client) is True

def test_none_client_defaults_to_true(self):
"""When client is None, has_new_settings_ui should not be called but
get_env_var should default to new UI instructions"""
state = MonitorState()
clear_env_var("MONITORED_CONTENT")
get_env_var("MONITORED_CONTENT", state, client=None)
assert len(state.instructions) == 1
assert "Settings</b>" in state.instructions[0]
assert "Advanced</b>" in state.instructions[0]

def test_exception_defaults_to_true(self):
"""When client.version raises, default to new UI instructions"""
client = MagicMock()
type(client).version = property(lambda self: (_ for _ in ()).throw(RuntimeError("fail")))
assert has_new_settings_ui(client) is True


class TestGetEnvVarVersionDetection:

def test_new_ui_instructions_for_new_version(self):
"""MONITORED_CONTENT instructions should reference new UI for >= 2026.03.0"""
state = MonitorState()
client = MagicMock()
client.version = "2026.03.0"
clear_env_var("MONITORED_CONTENT")

get_env_var("MONITORED_CONTENT", state, client=client)

assert len(state.instructions) == 1
assert "Settings</b>" in state.instructions[0]
assert "Advanced</b>" in state.instructions[0]
assert "settings-button.png" in state.instructions[0]
assert "env-vars.png" in state.instructions[0]

def test_old_ui_instructions_for_old_version(self):
"""MONITORED_CONTENT instructions should reference old UI for < 2026.03.0"""
state = MonitorState()
client = MagicMock()
client.version = "2025.04.0"
clear_env_var("MONITORED_CONTENT")

get_env_var("MONITORED_CONTENT", state, client=client)

assert len(state.instructions) == 1
assert "gear icon</b>" in state.instructions[0]
assert "Vars</b>" in state.instructions[0]
assert "settings-gear-icon.png" in state.instructions[0]
assert "vars.png" in state.instructions[0]

def test_non_monitored_content_var_ignores_client(self):
"""Non-MONITORED_CONTENT vars should not be affected by client version"""
state = MonitorState()
client = MagicMock()
client.version = "2025.04.0"
clear_env_var("SOME_OTHER_VAR")

get_env_var("SOME_OTHER_VAR", state, client=client)

assert len(state.instructions) == 1
assert "Please set the <code>SOME_OTHER_VAR</code>" in state.instructions[0]

Loading