"""Test telemetry functionality.""" from unittest.mock import MagicMock, patch import pytest from browser_use.config import CONFIG from browser_use.telemetry import ( CLITelemetryEvent, MCPClientTelemetryEvent, MCPServerTelemetryEvent, ProductTelemetry, ) from browser_use.utils import get_browser_use_version @pytest.fixture def reset_telemetry_singleton(): """Reset the telemetry singleton between tests.""" # The singleton decorator stores instance in a list at index 0 # We need to access the closure variable which stores the singleton instance # Import the actual module to access the wrapped function from browser_use.telemetry import service # Get the ProductTelemetry wrapper function created by @singleton wrapper_func = service.ProductTelemetry # Access the closure variable (instance list) and reset it # The closure contains the 'instance' list at index 0 # Type ignore needed because pyright doesn't know this is a wrapper function if hasattr(wrapper_func, '__closure__') and wrapper_func.__closure__: # type: ignore for cell in wrapper_func.__closure__: # type: ignore if hasattr(cell.cell_contents, '__setitem__'): # This is the instance list cell.cell_contents[0] = None break yield # Reset again after test if hasattr(wrapper_func, '__closure__') and wrapper_func.__closure__: # type: ignore for cell in wrapper_func.__closure__: # type: ignore if hasattr(cell.cell_contents, '__setitem__'): cell.cell_contents[0] = None break @pytest.fixture def mock_posthog(): """Mock PostHog client.""" with patch('browser_use.telemetry.service.Posthog') as mock: yield mock def test_telemetry_disabled_when_config_false(monkeypatch, reset_telemetry_singleton): """Test that telemetry is disabled when ANONYMIZED_TELEMETRY is False.""" # Set env var to disable telemetry monkeypatch.setattr(CONFIG, 'ANONYMIZED_TELEMETRY', False) # Create telemetry instance telemetry = ProductTelemetry() # Check that posthog client is None assert telemetry._posthog_client is None # Try to capture an event - should not fail event = CLITelemetryEvent( version=get_browser_use_version(), action='start', mode='interactive', ) telemetry.capture(event) # Should not raise def test_telemetry_enabled_when_config_true(monkeypatch, mock_posthog, reset_telemetry_singleton): """Test that telemetry is enabled when ANONYMIZED_TELEMETRY is True.""" # Set env var to enable telemetry monkeypatch.setattr(CONFIG, 'ANONYMIZED_TELEMETRY', True) # Create telemetry instance telemetry = ProductTelemetry() # Check that posthog client is created assert telemetry._posthog_client is not None mock_posthog.assert_called_once() def test_cli_telemetry_event(): """Test CLITelemetryEvent structure.""" event = CLITelemetryEvent( version='1.0.0', action='start', mode='interactive', model='gpt-4o', model_provider='OpenAI', duration_seconds=10.5, error_message=None, ) assert event.name == 'cli_event' assert event.version == '1.0.0' assert event.action == 'start' assert event.mode == 'interactive' assert event.model == 'gpt-4o' assert event.model_provider == 'OpenAI' assert event.duration_seconds == 10.5 assert event.error_message is None # Check properties props = event.properties assert 'version' in props assert 'action' in props assert 'mode' in props assert 'is_docker' in props # Docker context should be included assert isinstance(props['is_docker'], bool) # Should be a boolean assert 'name' not in props # name should not be in properties def test_mcp_client_telemetry_event(): """Test MCPClientTelemetryEvent structure.""" event = MCPClientTelemetryEvent( server_name='test-server', command='npx', tools_discovered=5, version='1.0.0', action='connect', tool_name='browser_navigate', duration_seconds=2.5, error_message=None, ) assert event.name == 'mcp_client_event' assert event.server_name == 'test-server' assert event.command == 'npx' assert event.tools_discovered == 5 assert event.version == '1.0.0' assert event.action == 'connect' assert event.tool_name == 'browser_navigate' assert event.duration_seconds == 2.5 assert event.error_message is None def test_mcp_server_telemetry_event(): """Test MCPServerTelemetryEvent structure.""" event = MCPServerTelemetryEvent( version='1.0.0', action='start', tool_name='browser_click', duration_seconds=1.2, error_message='Test error', ) assert event.name == 'mcp_server_event' assert event.version == '1.0.0' assert event.action == 'start' assert event.tool_name == 'browser_click' assert event.duration_seconds == 1.2 assert event.error_message == 'Test error' def test_telemetry_capture_with_mock(monkeypatch, reset_telemetry_singleton): """Test telemetry capture with mocked PostHog client.""" # Enable telemetry monkeypatch.setattr(CONFIG, 'ANONYMIZED_TELEMETRY', True) # Create mock posthog client mock_client = MagicMock() # Create telemetry instance and inject mock telemetry = ProductTelemetry() telemetry._posthog_client = mock_client # Capture an event event = CLITelemetryEvent( version='1.0.0', action='start', mode='oneshot', ) telemetry.capture(event) # Check that capture was called mock_client.capture.assert_called_once() call_args = mock_client.capture.call_args assert call_args[1]['event'] == 'cli_event' assert 'properties' in call_args[1] assert call_args[1]['properties']['version'] == '1.0.0' assert call_args[1]['properties']['action'] == 'start' assert call_args[1]['properties']['mode'] == 'oneshot' def test_telemetry_flush(monkeypatch, reset_telemetry_singleton): """Test telemetry flush method.""" # Enable telemetry monkeypatch.setattr(CONFIG, 'ANONYMIZED_TELEMETRY', True) # Create mock posthog client mock_client = MagicMock() # Create telemetry instance and inject mock telemetry = ProductTelemetry() telemetry._posthog_client = mock_client # Call flush telemetry.flush() # Check that flush was called mock_client.flush.assert_called_once() def test_telemetry_user_id_generation(tmp_path, monkeypatch, reset_telemetry_singleton): """Test that telemetry generates and persists user ID.""" # Set BROWSER_USE_CONFIG_DIR to temp directory config_dir = tmp_path / 'config' / 'browseruse' config_dir.mkdir(parents=True) monkeypatch.setenv('BROWSER_USE_CONFIG_DIR', str(config_dir)) # Enable telemetry monkeypatch.setattr(CONFIG, 'ANONYMIZED_TELEMETRY', True) # Create telemetry instance with patched path telemetry1 = ProductTelemetry() # Manually patch the USER_ID_PATH on the instance telemetry1.USER_ID_PATH = str(config_dir / 'device_id') user_id1 = telemetry1.user_id # Check that user ID is generated assert user_id1 != 'UNKNOWN_USER_ID' assert len(user_id1) > 0 # Create another instance - should get same ID telemetry2 = ProductTelemetry() telemetry2.USER_ID_PATH = str(config_dir / 'device_id') user_id2 = telemetry2.user_id assert user_id1 == user_id2 # Check that ID was persisted in config directory id_file = config_dir / 'device_id' assert id_file.exists() assert id_file.read_text() == user_id1 def test_mcp_server_telemetry_event_with_parent_process(): """Test MCPServerTelemetryEvent with parent_process_cmdline field.""" event = MCPServerTelemetryEvent( version='1.0.0', action='start', tool_name=None, duration_seconds=None, error_message=None, parent_process_cmdline='python -m browser_use.mcp.server', ) assert event.name == 'mcp_server_event' assert event.version == '1.0.0' assert event.action == 'start' assert event.parent_process_cmdline == 'python -m browser_use.mcp.server' # Check properties includes parent_process_cmdline props = event.properties assert 'parent_process_cmdline' in props assert props['parent_process_cmdline'] == 'python -m browser_use.mcp.server' assert 'is_docker' in props # Docker context should be included assert isinstance(props['is_docker'], bool) # Should be a boolean def test_telemetry_device_id_uses_config_dir(): """Test that telemetry device_id is stored in config directory, not cache directory.""" # This test verifies that the ProductTelemetry class uses CONFIG.BROWSER_USE_CONFIG_DIR # for the device_id file location instead of the cache directory. # Import ProductTelemetry to check the path from browser_use.telemetry.service import ProductTelemetry # Create telemetry instance telemetry = ProductTelemetry() # The USER_ID_PATH should use the config directory # We check that it contains the config dir path and ends with 'device_id' assert 'browseruse/device_id' in telemetry.USER_ID_PATH or 'browseruse\\device_id' in telemetry.USER_ID_PATH assert telemetry.USER_ID_PATH.endswith('device_id') # Verify it's not using the cache directory path assert 'cache' not in telemetry.USER_ID_PATH assert 'telemetry_user_id' not in telemetry.USER_ID_PATH