diff --git a/extensions/content-health-monitor/content-health-monitor.qmd b/extensions/content-health-monitor/content-health-monitor.qmd index 33a4598f..f276cdbb 100644 --- a/extensions/content-health-monitor/content-health-monitor.qmd +++ b/extensions/content-health-monitor/content-health-monitor.qmd @@ -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 @@ -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 @@ -60,6 +51,17 @@ if has_connect_env_vars: state.show_instructions = True state.instructions.append(f"Error initializing Connect client: {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 diff --git a/extensions/content-health-monitor/content_health_utils.py b/extensions/content-health-monitor/content_health_utils.py index e58db6a1..1b873452 100644 --- a/extensions/content-health-monitor/content_health_utils.py +++ b/extensions/content-health-monitor/content_health_utils.py @@ -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""" @@ -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 {var_name} environment variable." # Detailed instructions for MONITORED_CONTENT else: one_tab = "    " # For indentation in HTML - instruction = ( - f"To monitor a piece of content you must configure the {var_name} environment variable.

" - - f"

Step 1: Find the content to be monitored

" - f"{one_tab}• In a separate tab, open the content you wish to monitor
" - f"{one_tab}• In the browser address bar, copy the entire address (URL)

" - f"{one_tab}\"Browser
" - - f"

Step 2: Configure this report

" - f"{one_tab}• Return to this report
" - f"{one_tab}• Click the gear icon at the top right of the screen to open Content Settings
" - f"{one_tab}\"Content
" - f"{one_tab}• Click the Vars tab
" - f"{one_tab}• In the Name field enter {var_name}
" - f"{one_tab}• In the Value field paste the full address you copied in the previous step
" - f"{one_tab}• It should look like the example below
" - f"{one_tab}\"Environment
" - f"{one_tab}• Click Add Variable
" - f"{one_tab}• Click Save at the top of the screen to save your changes

" - - f"

Step 3: Run the report to execute the health check

" - f"{one_tab}• Click the Refresh Report button at the top right to run a health check against the monitored content
" - f"{one_tab}• The health check will run and report the status
" - f"{one_tab}• If the content is healthy, you will see a PASS status, otherwise you will see a FAIL status
" - f"{one_tab}• If you see a FAIL status, click the View Logs link to see more details about the failure
" - f"{one_tab}\"Refresh

" - - ) - + 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 {var_name} environment variable.

" + + f"

Step 1: Find the content to be monitored

" + f"{one_tab}• In a separate tab, open the content you wish to monitor
" + f"{one_tab}• In the browser address bar, copy the entire address (URL)

" + f"{one_tab}\"Browser
" + + f"

Step 2: Configure this report

" + f"{one_tab}• Return to this report
" + f"{one_tab}• Click Settings at the top right of the screen
" + f"{one_tab}\"Settings
" + f"{one_tab}• Click the Advanced tab
" + f"{one_tab}• In the Name field enter {var_name}
" + f"{one_tab}• In the Value field paste the full address you copied in the previous step
" + f"{one_tab}• It should look like the example below
" + f"{one_tab}\"Environment
" + f"{one_tab}• Click + Add
" + f"{one_tab}• Click Save to save your changes

" + + f"

Step 3: Run the report to execute the health check

" + f"{one_tab}• Click the Refresh button at the top right to run a health check against the monitored content
" + f"{one_tab}\"Refresh
" + f"{one_tab}• The health check will run and report the status
" + f"{one_tab}• If the content is healthy, you will see a PASS status, otherwise you will see a FAIL status
" + f"{one_tab}• If you see a FAIL status, click Logs to see more details about the failure

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

" + + f"

Step 1: Find the content to be monitored

" + f"{one_tab}• In a separate tab, open the content you wish to monitor
" + f"{one_tab}• In the browser address bar, copy the entire address (URL)

" + f"{one_tab}\"Browser
" + + f"

Step 2: Configure this report

" + f"{one_tab}• Return to this report
" + f"{one_tab}• Click the gear icon at the top right of the screen to open Content Settings
" + f"{one_tab}\"Content
" + f"{one_tab}• Click the Vars tab
" + f"{one_tab}• In the Name field enter {var_name}
" + f"{one_tab}• In the Value field paste the full address you copied in the previous step
" + f"{one_tab}• It should look like the example below
" + f"{one_tab}\"Environment
" + f"{one_tab}• Click Add Variable
" + f"{one_tab}• Click Save at the top of the screen to save your changes

" + + f"

Step 3: Run the report to execute the health check

" + f"{one_tab}• Click the Refresh Report button at the top right to run a health check against the monitored content
" + f"{one_tab}• The health check will run and report the status
" + f"{one_tab}• If the content is healthy, you will see a PASS status, otherwise you will see a FAIL status
" + f"{one_tab}• If you see a FAIL status, click the View Logs link to see more details about the failure
" + f"{one_tab}\"Refresh

" + ) + # Helper function to extract error messages from exceptions def format_error_message(exception): """Extract a clean error message from various exception types""" diff --git a/extensions/content-health-monitor/images/env-vars.png b/extensions/content-health-monitor/images/env-vars.png new file mode 100644 index 00000000..285c2150 Binary files /dev/null and b/extensions/content-health-monitor/images/env-vars.png differ diff --git a/extensions/content-health-monitor/images/refresh-report.png b/extensions/content-health-monitor/images/refresh-report.png index c2677294..f3e82829 100644 Binary files a/extensions/content-health-monitor/images/refresh-report.png and b/extensions/content-health-monitor/images/refresh-report.png differ diff --git a/extensions/content-health-monitor/images/settings-button.png b/extensions/content-health-monitor/images/settings-button.png new file mode 100644 index 00000000..5e0afb1f Binary files /dev/null and b/extensions/content-health-monitor/images/settings-button.png differ diff --git a/extensions/content-health-monitor/manifest.json b/extensions/content-health-monitor/manifest.json index a6e4b836..79280327 100644 --- a/extensions/content-health-monitor/manifest.json +++ b/extensions/content-health-monitor/manifest.json @@ -12,7 +12,7 @@ "category": "extension", "tags": [], "minimumConnectVersion": "2025.04.0", - "version": "1.0.0" + "version": "1.0.1" }, "environment": { "python": { @@ -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" } } } diff --git a/extensions/content-health-monitor/test_content_health_utils.py b/extensions/content-health-monitor/test_content_health_utils.py index 52577383..4005f2ca 100644 --- a/extensions/content-health-monitor/test_content_health_utils.py +++ b/extensions/content-health-monitor/test_content_health_utils.py @@ -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 @@ -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" in state.instructions[0] + assert "Advanced" 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" in state.instructions[0] + assert "Advanced" 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" in state.instructions[0] + assert "Vars" 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 SOME_OTHER_VAR" in state.instructions[0] +