From 874ae16598ac2da2fca50f4d7eefafd6e8faba2b Mon Sep 17 00:00:00 2001 From: Braelyn Boynton Date: Mon, 20 Jan 2025 15:43:47 -0800 Subject: [PATCH 1/4] assigned guid for cli user --- agentstack/telemetry.py | 31 +++++++++++++++++++++++++++++-- agentstack/update.py | 20 +++----------------- agentstack/utils.py | 14 ++++++++++++++ 3 files changed, 46 insertions(+), 19 deletions(-) diff --git a/agentstack/telemetry.py b/agentstack/telemetry.py index db6123ee..76449a21 100644 --- a/agentstack/telemetry.py +++ b/agentstack/telemetry.py @@ -21,17 +21,22 @@ # cool of you to allow telemetry <3 # # - braelyn +import json import os import platform import socket +import uuid +from pathlib import Path from typing import Optional import psutil import requests from agentstack import conf from agentstack.auth import get_stored_token -from agentstack.utils import get_telemetry_opt_out, get_framework, get_version +from agentstack.utils import get_telemetry_opt_out, get_framework, get_version, get_base_dir -TELEMETRY_URL = 'https://api.agentstack.sh/telemetry' +# TELEMETRY_URL = 'https://api.agentstack.sh/telemetry' +TELEMETRY_URL = 'http://localhost:3000/telemetry' +USER_GUID_FILE_PATH = get_base_dir() / ".cli-user-guid" def collect_machine_telemetry(command: str): @@ -46,6 +51,7 @@ def collect_machine_telemetry(command: str): 'cpu_count': psutil.cpu_count(logical=True), 'memory': psutil.virtual_memory().total, 'agentstack_version': get_version(), + 'cli_user_id': _get_cli_user_guid() } if command != "init": @@ -97,4 +103,25 @@ def update_telemetry(id: int, result: int, message: Optional[str] = None): try: requests.put(TELEMETRY_URL, json={"id": id, "result": result, "message": message}) except Exception: + pass + +def _get_cli_user_guid() -> str: + if Path(USER_GUID_FILE_PATH).exists(): + try: + with open(USER_GUID_FILE_PATH, 'r') as f: + return f.read() + except (json.JSONDecodeError, PermissionError): + return "unknown" + + # make new cli user guid + try: + # Create directory if it doesn't exist + USER_GUID_FILE_PATH.parent.mkdir(parents=True, exist_ok=True) + + guid = str(uuid.uuid4()) + with open(USER_GUID_FILE_PATH, 'w') as f: + f.write(guid) + return guid + except (OSError, PermissionError): + # Silently fail in CI or when we can't write pass \ No newline at end of file diff --git a/agentstack/update.py b/agentstack/update.py index d3d8dcac..2787e3d4 100644 --- a/agentstack/update.py +++ b/agentstack/update.py @@ -5,23 +5,8 @@ from packaging.version import parse as parse_version, Version import inquirer from agentstack import log -from agentstack.utils import term_color, get_version, get_framework +from agentstack.utils import term_color, get_version, get_framework, get_base_dir from agentstack import packaging -from appdirs import user_data_dir - - -def _get_base_dir(): - """Try to get appropriate directory for storing update file""" - try: - base_dir = Path(user_data_dir("agentstack", "agency")) - # Test if we can write to directory - test_file = base_dir / '.test_write_permission' - test_file.touch() - test_file.unlink() - except (RuntimeError, OSError, PermissionError): - # In CI or when directory is not writable, use temp directory - base_dir = Path(os.getenv('TEMP', '/tmp')) - return base_dir AGENTSTACK_PACKAGE = 'agentstack' @@ -35,7 +20,8 @@ def _get_base_dir(): 'TEAMCITY_VERSION', ] -LAST_CHECK_FILE_PATH = _get_base_dir() / ".cli-last-update" +LAST_CHECK_FILE_PATH = get_base_dir() / ".cli-last-update" +USER_GUID_FILE_PATH = get_base_dir() / ".cli-user-guid" INSTALL_PATH = Path(sys.executable).parent.parent ENDPOINT_URL = "https://pypi.org/simple" CHECK_EVERY = 3600 # hour diff --git a/agentstack/utils.py b/agentstack/utils.py index d68df3be..cc2569ed 100644 --- a/agentstack/utils.py +++ b/agentstack/utils.py @@ -8,6 +8,7 @@ import importlib.resources from agentstack import conf from inquirer import errors as inquirer_errors +from appdirs import user_data_dir def get_version(package: str = 'agentstack'): @@ -118,3 +119,16 @@ def validator(_, answer): return True return validator + +def get_base_dir(): + """Try to get appropriate directory for storing update file""" + try: + base_dir = Path(user_data_dir("agentstack", "agency")) + # Test if we can write to directory + test_file = base_dir / '.test_write_permission' + test_file.touch() + test_file.unlink() + except (RuntimeError, OSError, PermissionError): + # In CI or when directory is not writable, use temp directory + base_dir = Path(os.getenv('TEMP', '/tmp')) + return base_dir \ No newline at end of file From a98520b93d73e64164fdee12a1a977d556365121 Mon Sep 17 00:00:00 2001 From: Braelyn Boynton Date: Mon, 20 Jan 2025 15:44:25 -0800 Subject: [PATCH 2/4] fix telem url --- agentstack/telemetry.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/agentstack/telemetry.py b/agentstack/telemetry.py index 76449a21..7aa98a70 100644 --- a/agentstack/telemetry.py +++ b/agentstack/telemetry.py @@ -34,8 +34,7 @@ from agentstack.auth import get_stored_token from agentstack.utils import get_telemetry_opt_out, get_framework, get_version, get_base_dir -# TELEMETRY_URL = 'https://api.agentstack.sh/telemetry' -TELEMETRY_URL = 'http://localhost:3000/telemetry' +TELEMETRY_URL = 'https://api.agentstack.sh/telemetry' USER_GUID_FILE_PATH = get_base_dir() / ".cli-user-guid" From f730c021df00766a5160581d17c894f6a428b08a Mon Sep 17 00:00:00 2001 From: Braelyn Boynton Date: Mon, 20 Jan 2025 16:08:36 -0800 Subject: [PATCH 3/4] return statement --- agentstack/telemetry.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/agentstack/telemetry.py b/agentstack/telemetry.py index 7aa98a70..8b8f2fa0 100644 --- a/agentstack/telemetry.py +++ b/agentstack/telemetry.py @@ -123,4 +123,4 @@ def _get_cli_user_guid() -> str: return guid except (OSError, PermissionError): # Silently fail in CI or when we can't write - pass \ No newline at end of file + return "unknown" \ No newline at end of file From 7a1a25a345db7bccd909e518bbd343bd2c157a10 Mon Sep 17 00:00:00 2001 From: Braelyn Boynton Date: Tue, 21 Jan 2025 15:23:33 -0800 Subject: [PATCH 4/4] testing --- tests/test_telemetry.py | 72 ++++++++++++++++++++++++++++++++++++++++- tests/test_update.py | 27 ---------------- tests/test_utils.py | 35 +++++++++++++++++++- 3 files changed, 105 insertions(+), 29 deletions(-) diff --git a/tests/test_telemetry.py b/tests/test_telemetry.py index 35f3c5fa..75d9b58e 100644 --- a/tests/test_telemetry.py +++ b/tests/test_telemetry.py @@ -1,7 +1,10 @@ import os import unittest -from agentstack.utils import get_telemetry_opt_out +import uuid +from unittest.mock import patch, mock_open +from agentstack.telemetry import _get_cli_user_guid +from agentstack.utils import get_telemetry_opt_out class TelemetryTest(unittest.TestCase): def test_telemetry_opt_out_env_var_set(self): @@ -10,3 +13,70 @@ def test_telemetry_opt_out_env_var_set(self): def test_telemetry_opt_out_set_in_test_environment(self): assert get_telemetry_opt_out() + + @patch('pathlib.Path.exists') + @patch('builtins.open', new_callable=mock_open, read_data='existing-guid') + def test_existing_guid_file(self, mock_file, mock_exists): + """Test when GUID file exists and can be read successfully""" + mock_exists.return_value = True + + result = _get_cli_user_guid() + + self.assertEqual(result, 'existing-guid') + mock_exists.assert_called_once_with() + + @patch('pathlib.Path.exists') + @patch('pathlib.Path.mkdir') + @patch('uuid.uuid4') + @patch('builtins.open', new_callable=mock_open) + def test_create_new_guid(self, mock_file, mock_uuid, mock_mkdir, mock_exists): + """Test creation of new GUID when file doesn't exist""" + mock_exists.return_value = False + mock_uuid.return_value = uuid.UUID('12345678-1234-5678-1234-567812345678') + + result = _get_cli_user_guid() + + self.assertEqual(result, '12345678-1234-5678-1234-567812345678') + mock_exists.assert_called_once_with() + mock_mkdir.assert_called_once_with(parents=True, exist_ok=True) + handle = mock_file() + handle.write.assert_called_once_with('12345678-1234-5678-1234-567812345678') + + @patch('pathlib.Path.exists') + @patch('builtins.open') + def test_permission_error_on_read(self, mock_file, mock_exists): + """Test handling of PermissionError when reading file""" + mock_exists.return_value = True + mock_file.side_effect = PermissionError() + + result = _get_cli_user_guid() + + self.assertEqual(result, 'unknown') + mock_exists.assert_called_once_with() + + @patch('pathlib.Path.exists') + @patch('pathlib.Path.mkdir') + @patch('builtins.open') + def test_permission_error_on_write(self, mock_file, mock_mkdir, mock_exists): + """Test handling of PermissionError when writing new file""" + mock_exists.return_value = False + mock_file.side_effect = PermissionError() + + result = _get_cli_user_guid() + + self.assertEqual(result, 'unknown') + mock_exists.assert_called_once_with() + mock_mkdir.assert_called_once_with(parents=True, exist_ok=True) + + @patch('pathlib.Path.exists') + @patch('pathlib.Path.mkdir') + def test_os_error_on_mkdir(self, mock_mkdir, mock_exists): + """Test handling of OSError when creating directory""" + mock_exists.return_value = False + mock_mkdir.side_effect = OSError() + + result = _get_cli_user_guid() + + self.assertEqual(result, 'unknown') + mock_exists.assert_called_once_with() + mock_mkdir.assert_called_once_with(parents=True, exist_ok=True) \ No newline at end of file diff --git a/tests/test_update.py b/tests/test_update.py index af25429e..e08ca2d7 100644 --- a/tests/test_update.py +++ b/tests/test_update.py @@ -10,7 +10,6 @@ _is_ci_environment, CI_ENV_VARS, should_update, - _get_base_dir, get_latest_version, AGENTSTACK_PACKAGE, load_update_data, @@ -46,32 +45,6 @@ def test_updates_disabled_by_env_var_in_test(self): with patch.dict('os.environ', {'AGENTSTACK_UPDATE_DISABLE': 'true'}): self.assertFalse(should_update()) - @patch('agentstack.update.user_data_dir') - def test_get_base_dir_writable(self, mock_user_data_dir): - """ - Test that _get_base_dir() returns a writable Path when user_data_dir is accessible. - """ - mock_path = '/mock/user/data/dir' - mock_user_data_dir.return_value = mock_path - - result = _get_base_dir() - - self.assertIsInstance(result, Path) - self.assertTrue(result.is_absolute()) - - @patch('agentstack.update.user_data_dir') - def test_get_base_dir_not_writable(self, mock_user_data_dir): - """ - Test that _get_base_dir() falls back to a temporary directory when user_data_dir is not writable. - """ - mock_user_data_dir.side_effect = PermissionError - - result = _get_base_dir() - - self.assertIsInstance(result, Path) - self.assertTrue(result.is_absolute()) - self.assertIn(str(result), ['/tmp', os.environ.get('TEMP', '/tmp')]) - def test_get_latest_version(self): """ Test that get_latest_version returns a valid Version object from the actual PyPI. diff --git a/tests/test_utils.py b/tests/test_utils.py index 6b210ab7..a938c41c 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -1,6 +1,14 @@ +import os import unittest +from pathlib import Path +from unittest.mock import patch -from agentstack.utils import clean_input, is_snake_case, validator_not_empty +from agentstack.utils import ( + clean_input, + is_snake_case, + validator_not_empty, + get_base_dir +) from inquirer import errors as inquirer_errors @@ -37,3 +45,28 @@ def test_validator_not_empty(self): with self.assertRaises(inquirer_errors.ValidationError): validator(None, "ab") + @patch('agentstack.utils.user_data_dir') + def test_get_base_dir_not_writable(self, mock_user_data_dir): + """ + Test that get_base_dir() falls back to a temporary directory when user_data_dir is not writable. + """ + mock_user_data_dir.side_effect = PermissionError + + result = get_base_dir() + + self.assertIsInstance(result, Path) + self.assertTrue(result.is_absolute()) + self.assertIn(str(result), ['/tmp', os.environ.get('TEMP', '/tmp')]) + + @patch('agentstack.utils.user_data_dir') + def test_get_base_dir_writable(self, mock_user_data_dir): + """ + Test that get_base_dir() returns a writable Path when user_data_dir is accessible. + """ + mock_path = '/mock/user/data/dir' + mock_user_data_dir.return_value = mock_path + + result = get_base_dir() + + self.assertIsInstance(result, Path) + self.assertTrue(result.is_absolute()) \ No newline at end of file