Skip to content

Commit 57a8cf0

Browse files
authored
Merge pull request #226 from AgentOps-AI/user-guid
assign user/cli guid
2 parents bb50b4a + 7a1a25a commit 57a8cf0

File tree

6 files changed

+150
-48
lines changed

6 files changed

+150
-48
lines changed

agentstack/telemetry.py

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,17 +21,21 @@
2121
# cool of you to allow telemetry <3
2222
#
2323
# - braelyn
24+
import json
2425
import os
2526
import platform
2627
import socket
28+
import uuid
29+
from pathlib import Path
2730
from typing import Optional
2831
import psutil
2932
import requests
3033
from agentstack import conf
3134
from agentstack.auth import get_stored_token
32-
from agentstack.utils import get_telemetry_opt_out, get_framework, get_version
35+
from agentstack.utils import get_telemetry_opt_out, get_framework, get_version, get_base_dir
3336

3437
TELEMETRY_URL = 'https://api.agentstack.sh/telemetry'
38+
USER_GUID_FILE_PATH = get_base_dir() / ".cli-user-guid"
3539

3640

3741
def collect_machine_telemetry(command: str):
@@ -46,6 +50,7 @@ def collect_machine_telemetry(command: str):
4650
'cpu_count': psutil.cpu_count(logical=True),
4751
'memory': psutil.virtual_memory().total,
4852
'agentstack_version': get_version(),
53+
'cli_user_id': _get_cli_user_guid()
4954
}
5055

5156
if command != "init":
@@ -97,4 +102,25 @@ def update_telemetry(id: int, result: int, message: Optional[str] = None):
97102
try:
98103
requests.put(TELEMETRY_URL, json={"id": id, "result": result, "message": message})
99104
except Exception:
100-
pass
105+
pass
106+
107+
def _get_cli_user_guid() -> str:
108+
if Path(USER_GUID_FILE_PATH).exists():
109+
try:
110+
with open(USER_GUID_FILE_PATH, 'r') as f:
111+
return f.read()
112+
except (json.JSONDecodeError, PermissionError):
113+
return "unknown"
114+
115+
# make new cli user guid
116+
try:
117+
# Create directory if it doesn't exist
118+
USER_GUID_FILE_PATH.parent.mkdir(parents=True, exist_ok=True)
119+
120+
guid = str(uuid.uuid4())
121+
with open(USER_GUID_FILE_PATH, 'w') as f:
122+
f.write(guid)
123+
return guid
124+
except (OSError, PermissionError):
125+
# Silently fail in CI or when we can't write
126+
return "unknown"

agentstack/update.py

Lines changed: 3 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -5,23 +5,8 @@
55
from packaging.version import parse as parse_version, Version
66
import inquirer
77
from agentstack import log
8-
from agentstack.utils import term_color, get_version, get_framework
8+
from agentstack.utils import term_color, get_version, get_framework, get_base_dir
99
from agentstack import packaging
10-
from appdirs import user_data_dir
11-
12-
13-
def _get_base_dir():
14-
"""Try to get appropriate directory for storing update file"""
15-
try:
16-
base_dir = Path(user_data_dir("agentstack", "agency"))
17-
# Test if we can write to directory
18-
test_file = base_dir / '.test_write_permission'
19-
test_file.touch()
20-
test_file.unlink()
21-
except (RuntimeError, OSError, PermissionError):
22-
# In CI or when directory is not writable, use temp directory
23-
base_dir = Path(os.getenv('TEMP', '/tmp'))
24-
return base_dir
2510

2611

2712
AGENTSTACK_PACKAGE = 'agentstack'
@@ -35,7 +20,8 @@ def _get_base_dir():
3520
'TEAMCITY_VERSION',
3621
]
3722

38-
LAST_CHECK_FILE_PATH = _get_base_dir() / ".cli-last-update"
23+
LAST_CHECK_FILE_PATH = get_base_dir() / ".cli-last-update"
24+
USER_GUID_FILE_PATH = get_base_dir() / ".cli-user-guid"
3925
INSTALL_PATH = Path(sys.executable).parent.parent
4026
ENDPOINT_URL = "https://pypi.org/simple"
4127
CHECK_EVERY = 3600 # hour

agentstack/utils.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
import importlib.resources
99
from agentstack import conf
1010
from inquirer import errors as inquirer_errors
11+
from appdirs import user_data_dir
1112

1213

1314
def get_version(package: str = 'agentstack'):
@@ -118,3 +119,16 @@ def validator(_, answer):
118119
return True
119120

120121
return validator
122+
123+
def get_base_dir():
124+
"""Try to get appropriate directory for storing update file"""
125+
try:
126+
base_dir = Path(user_data_dir("agentstack", "agency"))
127+
# Test if we can write to directory
128+
test_file = base_dir / '.test_write_permission'
129+
test_file.touch()
130+
test_file.unlink()
131+
except (RuntimeError, OSError, PermissionError):
132+
# In CI or when directory is not writable, use temp directory
133+
base_dir = Path(os.getenv('TEMP', '/tmp'))
134+
return base_dir

tests/test_telemetry.py

Lines changed: 71 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
import os
22
import unittest
3-
from agentstack.utils import get_telemetry_opt_out
3+
import uuid
4+
from unittest.mock import patch, mock_open
45

6+
from agentstack.telemetry import _get_cli_user_guid
7+
from agentstack.utils import get_telemetry_opt_out
58

69
class TelemetryTest(unittest.TestCase):
710
def test_telemetry_opt_out_env_var_set(self):
@@ -10,3 +13,70 @@ def test_telemetry_opt_out_env_var_set(self):
1013

1114
def test_telemetry_opt_out_set_in_test_environment(self):
1215
assert get_telemetry_opt_out()
16+
17+
@patch('pathlib.Path.exists')
18+
@patch('builtins.open', new_callable=mock_open, read_data='existing-guid')
19+
def test_existing_guid_file(self, mock_file, mock_exists):
20+
"""Test when GUID file exists and can be read successfully"""
21+
mock_exists.return_value = True
22+
23+
result = _get_cli_user_guid()
24+
25+
self.assertEqual(result, 'existing-guid')
26+
mock_exists.assert_called_once_with()
27+
28+
@patch('pathlib.Path.exists')
29+
@patch('pathlib.Path.mkdir')
30+
@patch('uuid.uuid4')
31+
@patch('builtins.open', new_callable=mock_open)
32+
def test_create_new_guid(self, mock_file, mock_uuid, mock_mkdir, mock_exists):
33+
"""Test creation of new GUID when file doesn't exist"""
34+
mock_exists.return_value = False
35+
mock_uuid.return_value = uuid.UUID('12345678-1234-5678-1234-567812345678')
36+
37+
result = _get_cli_user_guid()
38+
39+
self.assertEqual(result, '12345678-1234-5678-1234-567812345678')
40+
mock_exists.assert_called_once_with()
41+
mock_mkdir.assert_called_once_with(parents=True, exist_ok=True)
42+
handle = mock_file()
43+
handle.write.assert_called_once_with('12345678-1234-5678-1234-567812345678')
44+
45+
@patch('pathlib.Path.exists')
46+
@patch('builtins.open')
47+
def test_permission_error_on_read(self, mock_file, mock_exists):
48+
"""Test handling of PermissionError when reading file"""
49+
mock_exists.return_value = True
50+
mock_file.side_effect = PermissionError()
51+
52+
result = _get_cli_user_guid()
53+
54+
self.assertEqual(result, 'unknown')
55+
mock_exists.assert_called_once_with()
56+
57+
@patch('pathlib.Path.exists')
58+
@patch('pathlib.Path.mkdir')
59+
@patch('builtins.open')
60+
def test_permission_error_on_write(self, mock_file, mock_mkdir, mock_exists):
61+
"""Test handling of PermissionError when writing new file"""
62+
mock_exists.return_value = False
63+
mock_file.side_effect = PermissionError()
64+
65+
result = _get_cli_user_guid()
66+
67+
self.assertEqual(result, 'unknown')
68+
mock_exists.assert_called_once_with()
69+
mock_mkdir.assert_called_once_with(parents=True, exist_ok=True)
70+
71+
@patch('pathlib.Path.exists')
72+
@patch('pathlib.Path.mkdir')
73+
def test_os_error_on_mkdir(self, mock_mkdir, mock_exists):
74+
"""Test handling of OSError when creating directory"""
75+
mock_exists.return_value = False
76+
mock_mkdir.side_effect = OSError()
77+
78+
result = _get_cli_user_guid()
79+
80+
self.assertEqual(result, 'unknown')
81+
mock_exists.assert_called_once_with()
82+
mock_mkdir.assert_called_once_with(parents=True, exist_ok=True)

tests/test_update.py

Lines changed: 0 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@
1010
_is_ci_environment,
1111
CI_ENV_VARS,
1212
should_update,
13-
_get_base_dir,
1413
get_latest_version,
1514
AGENTSTACK_PACKAGE,
1615
load_update_data,
@@ -46,32 +45,6 @@ def test_updates_disabled_by_env_var_in_test(self):
4645
with patch.dict('os.environ', {'AGENTSTACK_UPDATE_DISABLE': 'true'}):
4746
self.assertFalse(should_update())
4847

49-
@patch('agentstack.update.user_data_dir')
50-
def test_get_base_dir_writable(self, mock_user_data_dir):
51-
"""
52-
Test that _get_base_dir() returns a writable Path when user_data_dir is accessible.
53-
"""
54-
mock_path = '/mock/user/data/dir'
55-
mock_user_data_dir.return_value = mock_path
56-
57-
result = _get_base_dir()
58-
59-
self.assertIsInstance(result, Path)
60-
self.assertTrue(result.is_absolute())
61-
62-
@patch('agentstack.update.user_data_dir')
63-
def test_get_base_dir_not_writable(self, mock_user_data_dir):
64-
"""
65-
Test that _get_base_dir() falls back to a temporary directory when user_data_dir is not writable.
66-
"""
67-
mock_user_data_dir.side_effect = PermissionError
68-
69-
result = _get_base_dir()
70-
71-
self.assertIsInstance(result, Path)
72-
self.assertTrue(result.is_absolute())
73-
self.assertIn(str(result), ['/tmp', os.environ.get('TEMP', '/tmp')])
74-
7548
def test_get_latest_version(self):
7649
"""
7750
Test that get_latest_version returns a valid Version object from the actual PyPI.

tests/test_utils.py

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,14 @@
1+
import os
12
import unittest
3+
from pathlib import Path
4+
from unittest.mock import patch
25

3-
from agentstack.utils import clean_input, is_snake_case, validator_not_empty
6+
from agentstack.utils import (
7+
clean_input,
8+
is_snake_case,
9+
validator_not_empty,
10+
get_base_dir
11+
)
412
from inquirer import errors as inquirer_errors
513

614

@@ -37,3 +45,28 @@ def test_validator_not_empty(self):
3745
with self.assertRaises(inquirer_errors.ValidationError):
3846
validator(None, "ab")
3947

48+
@patch('agentstack.utils.user_data_dir')
49+
def test_get_base_dir_not_writable(self, mock_user_data_dir):
50+
"""
51+
Test that get_base_dir() falls back to a temporary directory when user_data_dir is not writable.
52+
"""
53+
mock_user_data_dir.side_effect = PermissionError
54+
55+
result = get_base_dir()
56+
57+
self.assertIsInstance(result, Path)
58+
self.assertTrue(result.is_absolute())
59+
self.assertIn(str(result), ['/tmp', os.environ.get('TEMP', '/tmp')])
60+
61+
@patch('agentstack.utils.user_data_dir')
62+
def test_get_base_dir_writable(self, mock_user_data_dir):
63+
"""
64+
Test that get_base_dir() returns a writable Path when user_data_dir is accessible.
65+
"""
66+
mock_path = '/mock/user/data/dir'
67+
mock_user_data_dir.return_value = mock_path
68+
69+
result = get_base_dir()
70+
71+
self.assertIsInstance(result, Path)
72+
self.assertTrue(result.is_absolute())

0 commit comments

Comments
 (0)