diff --git a/src/xpk/core/config.py b/src/xpk/core/config.py index 816e9b96f..44055075d 100644 --- a/src/xpk/core/config.py +++ b/src/xpk/core/config.py @@ -84,7 +84,7 @@ def _save_configs(self, config_yaml: dict) -> None: with open(self._config, encoding='utf-8', mode='w') as stream: yaml.dump(config_yaml, stream) - def set(self, key: str, value: str) -> None: + def set(self, key: str, value: str | None) -> None: if key not in self._allowed_keys: xpk_print(f'Key {key} is not an allowed xpk config key.') return diff --git a/src/xpk/core/telemetry.py b/src/xpk/core/telemetry.py index 4e57774ae..216d2e82c 100644 --- a/src/xpk/core/telemetry.py +++ b/src/xpk/core/telemetry.py @@ -105,17 +105,6 @@ def _clearcut_flush(file_path: str) -> None: os.remove(file_path) -def ensure_client_id() -> str: - """Generates Client ID and stores in configuration if not already present.""" - current_client_id = xpk_config.get(CLIENT_ID_KEY) - if current_client_id is not None: - return current_client_id - - new_client_id = str(uuid.uuid4()) - xpk_config.set(CLIENT_ID_KEY, new_client_id) - return new_client_id - - class MetricsEventMetadataKey(Enum): SESSION_ID = "XPK_SESSION_ID" DRY_RUN = "XPK_DRY_RUN" @@ -125,6 +114,8 @@ class MetricsEventMetadataKey(Enum): PROVISIONING_MODE = "XPK_PROVISIONING_MODE" COMMAND = "XPK_COMMAND" EXIT_CODE = "XPK_EXIT_CODE" + RUNNING_AS_PIP = "XPK_RUNNING_AS_PIP" + RUNNING_FROM_SOURCE = "XPK_RUNNING_FROM_SOURCE" @dataclass @@ -222,6 +213,10 @@ def _get_base_event_metadata() -> dict[MetricsEventMetadataKey, str]: MetricsEventMetadataKey.SESSION_ID: _get_session_id(), MetricsEventMetadataKey.DRY_RUN: str(is_dry_run()).lower(), MetricsEventMetadataKey.PYTHON_VERSION: platform.python_version(), + MetricsEventMetadataKey.RUNNING_AS_PIP: str(_is_running_as_pip()).lower(), + MetricsEventMetadataKey.RUNNING_FROM_SOURCE: str( + _is_running_from_source() + ).lower(), } @@ -229,9 +224,32 @@ def _get_base_concord_event() -> dict[str, str]: return { "release_version": xpk_version, "console_type": "XPK", - "client_install_id": ensure_client_id(), + "client_install_id": _ensure_client_id(), } +def _is_running_as_pip() -> bool: + return os.path.basename(sys.argv[0]) == "xpk" + + +def _is_running_from_source() -> bool: + current_path = os.path.abspath(os.path.realpath(__file__)) + return ( + "site-packages" not in current_path + and "dist-packages" not in current_path + ) + + def _get_session_id() -> str: return str(uuid.uuid4()) + + +def _ensure_client_id() -> str: + """Generates Client ID and stores in configuration if not already present.""" + current_client_id = xpk_config.get(CLIENT_ID_KEY) + if current_client_id is not None: + return current_client_id + + new_client_id = str(uuid.uuid4()) + xpk_config.set(CLIENT_ID_KEY, new_client_id) + return new_client_id diff --git a/src/xpk/core/telemetry_test.py b/src/xpk/core/telemetry_test.py index 3e78d7e2a..488b03471 100644 --- a/src/xpk/core/telemetry_test.py +++ b/src/xpk/core/telemetry_test.py @@ -17,33 +17,24 @@ import pytest import json from .config import xpk_config, CLIENT_ID_KEY -from .telemetry import ensure_client_id, MetricsCollector, MetricsEventMetadataKey +from .telemetry import MetricsCollector, MetricsEventMetadataKey from ..utils.execution_context import set_dry_run +from pytest_mock import MockerFixture @pytest.fixture(autouse=True) -def setup_mocks(mocker): +def setup_mocks(mocker: MockerFixture): mocker.patch('xpk.core.telemetry._get_session_id', return_value='321231') mocker.patch('time.time', return_value=0) mocker.patch('platform.python_version', return_value='99.99.99') + mocker.patch('os.path.basename', return_value='xpk.py') + mocker.patch('os.path.abspath', return_value='/home/xpk_user') + set_dry_run(False) xpk_config.set(CLIENT_ID_KEY, 'client_id') yield xpk_config.set(CLIENT_ID_KEY, None) -def test_ensure_client_id_generates_client_id_when_its_not_present(): - xpk_config.set(CLIENT_ID_KEY, None) - ensure_client_id() - assert xpk_config.get(CLIENT_ID_KEY) is not None - - -def test_ensure_client_id_does_not_regenerate_id_when_its_present(): - client_id = '1337' - xpk_config.set(CLIENT_ID_KEY, client_id) - ensure_client_id() - assert xpk_config.get(CLIENT_ID_KEY) == client_id - - def test_metrics_collector_generates_client_id_if_not_present(): xpk_config.set(CLIENT_ID_KEY, None) MetricsCollector.log_start(command='test') @@ -54,7 +45,6 @@ def test_metrics_collector_generates_client_id_if_not_present(): def test_metrics_collector_logs_start_event_correctly(): - set_dry_run(False) MetricsCollector.log_start(command='test') payload = json.loads(MetricsCollector.flush()) extension_json = json.loads(payload['log_event'][0]['source_extension_json']) @@ -65,6 +55,8 @@ def test_metrics_collector_logs_start_event_correctly(): {'key': 'XPK_SESSION_ID', 'value': '321231'}, {'key': 'XPK_DRY_RUN', 'value': 'false'}, {'key': 'XPK_PYTHON_VERSION', 'value': '99.99.99'}, + {'key': 'XPK_RUNNING_AS_PIP', 'value': 'false'}, + {'key': 'XPK_RUNNING_FROM_SOURCE', 'value': 'true'}, {'key': 'XPK_COMMAND', 'value': 'test'}, ], 'event_name': 'start', @@ -73,8 +65,16 @@ def test_metrics_collector_logs_start_event_correctly(): } +def test_metrics_collector_generates_client_id_when_not_present(): + xpk_config.set(CLIENT_ID_KEY, None) + MetricsCollector.log_start(command='test') + payload = json.loads(MetricsCollector.flush()) + extension_json = json.loads(payload['log_event'][0]['source_extension_json']) + assert extension_json['client_install_id'] is not None + assert len(extension_json['client_install_id']) > 0 + + def test_metrics_collector_logs_complete_event_correctly(): - set_dry_run(True) MetricsCollector.log_complete(exit_code=2) payload = json.loads(MetricsCollector.flush()) extension_json = json.loads(payload['log_event'][0]['source_extension_json']) @@ -83,8 +83,10 @@ def test_metrics_collector_logs_complete_event_correctly(): 'console_type': 'XPK', 'event_metadata': [ {'key': 'XPK_SESSION_ID', 'value': '321231'}, - {'key': 'XPK_DRY_RUN', 'value': 'true'}, + {'key': 'XPK_DRY_RUN', 'value': 'false'}, {'key': 'XPK_PYTHON_VERSION', 'value': '99.99.99'}, + {'key': 'XPK_RUNNING_AS_PIP', 'value': 'false'}, + {'key': 'XPK_RUNNING_FROM_SOURCE', 'value': 'true'}, {'key': 'XPK_EXIT_CODE', 'value': '2'}, ], 'event_name': 'complete', @@ -94,7 +96,6 @@ def test_metrics_collector_logs_complete_event_correctly(): def test_metrics_collector_logs_custom_event_correctly(): - set_dry_run(False) MetricsCollector.log_custom( name='test', metadata={MetricsEventMetadataKey.PROVISIONING_MODE: 'flex'} ) @@ -107,6 +108,8 @@ def test_metrics_collector_logs_custom_event_correctly(): {'key': 'XPK_SESSION_ID', 'value': '321231'}, {'key': 'XPK_DRY_RUN', 'value': 'false'}, {'key': 'XPK_PYTHON_VERSION', 'value': '99.99.99'}, + {'key': 'XPK_RUNNING_AS_PIP', 'value': 'false'}, + {'key': 'XPK_RUNNING_FROM_SOURCE', 'value': 'true'}, {'key': 'XPK_PROVISIONING_MODE', 'value': 'flex'}, ], 'event_name': 'test', @@ -134,3 +137,57 @@ def test_metrics_collector_does_not_flush_event_twice(): MetricsCollector.log_start(command='version') payload = json.loads(MetricsCollector.flush()) assert len(payload['log_event']) == 1 + + +@pytest.mark.parametrize( + argnames='dry_run,expected', argvalues=[(False, 'false'), (True, 'true')] +) +def test_metrics_collector_logs_correct_dry_run_value( + dry_run: bool, expected: str +): + set_dry_run(dry_run) + MetricsCollector.log_start(command='test') + payload = MetricsCollector.flush() + assert _get_metadata_value(payload, 'XPK_DRY_RUN') == expected + + +@pytest.mark.parametrize( + argnames='basename,expected', + argvalues=[ + ('xpk', 'true'), + ('xpk.py', 'false'), + ], +) +def test_metrics_collectors_logs_correct_running_as_pip_value( + basename: str, expected: str, mocker: MockerFixture +): + mocker.patch('os.path.basename', return_value=basename) + MetricsCollector.log_start(command='test') + payload = MetricsCollector.flush() + assert _get_metadata_value(payload, 'XPK_RUNNING_AS_PIP') == expected + + +@pytest.mark.parametrize( + argnames='abspath,expected', + argvalues=[ + ('/site-packages/', 'false'), + ('/dist-packages/', 'false'), + ('/home/xpk_user', 'true'), + ], +) +def test_metrics_collectors_logs_correct_running_from_source_value( + abspath: str, expected: str, mocker: MockerFixture +): + mocker.patch('os.path.abspath', return_value=abspath) + MetricsCollector.log_start(command='test') + payload = MetricsCollector.flush() + assert _get_metadata_value(payload, 'XPK_RUNNING_FROM_SOURCE') == expected + + +def _get_metadata_value(payload_str: str, key: str) -> str | None: + payload = json.loads(payload_str) + metadata = json.loads(payload['log_event'][0]['source_extension_json'])[ + 'event_metadata' + ] + matching = (item['value'] for item in metadata if item['key'] == key) + return next(matching, None)