diff --git a/lib/crewai/src/crewai/telemetry/telemetry.py b/lib/crewai/src/crewai/telemetry/telemetry.py index 04303fc3d..133381975 100644 --- a/lib/crewai/src/crewai/telemetry/telemetry.py +++ b/lib/crewai/src/crewai/telemetry/telemetry.py @@ -34,6 +34,7 @@ from opentelemetry.trace import Span from typing_extensions import Self from crewai.events.event_bus import crewai_event_bus +from crewai.events.listeners.tracing.utils import has_user_declined_tracing from crewai.events.types.system_events import ( SigContEvent, SigHupEvent, @@ -144,12 +145,30 @@ class Telemetry: @classmethod def _is_telemetry_disabled(cls) -> bool: - """Check if telemetry should be disabled based on environment variables.""" - return ( + """Check if telemetry should be disabled based on environment variables. + + Telemetry is disabled when any of the following conditions are met: + - OTEL_SDK_DISABLED is set to 'true' + - CREWAI_DISABLE_TELEMETRY is set to 'true' + - CREWAI_DISABLE_TRACKING is set to 'true' + - CREWAI_TRACING_ENABLED is explicitly set to 'false' or '0' + - The user has explicitly declined tracing via the first-time prompt + """ + if ( os.getenv("OTEL_SDK_DISABLED", "false").lower() == "true" or os.getenv("CREWAI_DISABLE_TELEMETRY", "false").lower() == "true" or os.getenv("CREWAI_DISABLE_TRACKING", "false").lower() == "true" - ) + ): + return True + + tracing_env = os.getenv("CREWAI_TRACING_ENABLED", "").lower() + if tracing_env in ("false", "0"): + return True + + if has_user_declined_tracing(): + return True + + return False def _should_execute_telemetry(self) -> bool: """Check if telemetry operations should be executed.""" diff --git a/lib/crewai/tests/telemetry/test_execution_span_assignment.py b/lib/crewai/tests/telemetry/test_execution_span_assignment.py index e8abd5cc5..42201c0f2 100644 --- a/lib/crewai/tests/telemetry/test_execution_span_assignment.py +++ b/lib/crewai/tests/telemetry/test_execution_span_assignment.py @@ -16,9 +16,11 @@ def cleanup_singletons(): """Reset singletons between tests and enable telemetry.""" original_telemetry = os.environ.get("CREWAI_DISABLE_TELEMETRY") original_otel = os.environ.get("OTEL_SDK_DISABLED") + original_tracing = os.environ.get("CREWAI_TRACING_ENABLED") os.environ["CREWAI_DISABLE_TELEMETRY"] = "false" os.environ["OTEL_SDK_DISABLED"] = "false" + os.environ.pop("CREWAI_TRACING_ENABLED", None) with crewai_event_bus._rwlock.w_locked(): crewai_event_bus._sync_handlers.clear() @@ -45,6 +47,11 @@ def cleanup_singletons(): else: os.environ.pop("OTEL_SDK_DISABLED", None) + if original_tracing is not None: + os.environ["CREWAI_TRACING_ENABLED"] = original_tracing + else: + os.environ.pop("CREWAI_TRACING_ENABLED", None) + Telemetry._instance = None EventListener._instance = None if hasattr(Telemetry, "_lock"): diff --git a/lib/crewai/tests/telemetry/test_flow_crew_span_integration.py b/lib/crewai/tests/telemetry/test_flow_crew_span_integration.py index 80316cdb6..aa610ad00 100644 --- a/lib/crewai/tests/telemetry/test_flow_crew_span_integration.py +++ b/lib/crewai/tests/telemetry/test_flow_crew_span_integration.py @@ -55,9 +55,11 @@ def enable_telemetry_for_tests(): original_telemetry = os.environ.get("CREWAI_DISABLE_TELEMETRY") original_otel = os.environ.get("OTEL_SDK_DISABLED") + original_tracing = os.environ.get("CREWAI_TRACING_ENABLED") os.environ["CREWAI_DISABLE_TELEMETRY"] = "false" os.environ["OTEL_SDK_DISABLED"] = "false" + os.environ.pop("CREWAI_TRACING_ENABLED", None) with crewai_event_bus._rwlock.w_locked(): crewai_event_bus._sync_handlers.clear() @@ -89,6 +91,11 @@ def enable_telemetry_for_tests(): else: os.environ.pop("OTEL_SDK_DISABLED", None) + if original_tracing is not None: + os.environ["CREWAI_TRACING_ENABLED"] = original_tracing + else: + os.environ.pop("CREWAI_TRACING_ENABLED", None) + def test_crew_execution_span_in_flow_with_share_crew(): """Test that crew._execution_span is properly set when crew runs inside a flow. @@ -299,4 +306,4 @@ async def test_crew_execution_span_in_async_flow(): await flow.kickoff_async() assert flow.state.result != "" - mock_llm.call.assert_called() \ No newline at end of file + mock_llm.call.assert_called() diff --git a/lib/crewai/tests/telemetry/test_telemetry.py b/lib/crewai/tests/telemetry/test_telemetry.py index 8f7f5fc70..cd05aff5e 100644 --- a/lib/crewai/tests/telemetry/test_telemetry.py +++ b/lib/crewai/tests/telemetry/test_telemetry.py @@ -10,6 +10,9 @@ from opentelemetry import trace @pytest.fixture(autouse=True) def cleanup_telemetry(): + original_tracing = os.environ.get("CREWAI_TRACING_ENABLED") + os.environ.pop("CREWAI_TRACING_ENABLED", None) + Telemetry._instance = None if hasattr(Telemetry, "_lock"): Telemetry._lock = threading.Lock() @@ -18,6 +21,11 @@ def cleanup_telemetry(): if hasattr(Telemetry, "_lock"): Telemetry._lock = threading.Lock() + if original_tracing is not None: + os.environ["CREWAI_TRACING_ENABLED"] = original_tracing + else: + os.environ.pop("CREWAI_TRACING_ENABLED", None) + @pytest.mark.parametrize( "env_var,value,expected_ready", @@ -37,9 +45,10 @@ def test_telemetry_environment_variables(env_var, value, expected_ready): "OTEL_SDK_DISABLED": "false", "CREWAI_DISABLE_TELEMETRY": "false", "CREWAI_DISABLE_TRACKING": "false", + "CREWAI_TRACING_ENABLED": "", env_var: value, } - with patch.dict(os.environ, env_overrides): + with patch.dict(os.environ, env_overrides, clear=True): with patch("crewai.telemetry.telemetry.TracerProvider"): telemetry = Telemetry() assert telemetry.ready is expected_ready diff --git a/lib/crewai/tests/telemetry/test_telemetry_disable.py b/lib/crewai/tests/telemetry/test_telemetry_disable.py index 1357b338f..1ef1cb06b 100644 --- a/lib/crewai/tests/telemetry/test_telemetry_disable.py +++ b/lib/crewai/tests/telemetry/test_telemetry_disable.py @@ -2,6 +2,7 @@ import os from unittest.mock import MagicMock, patch import pytest +from crewai.events.listeners.tracing.utils import _tracing_enabled from crewai.telemetry import Telemetry @@ -9,7 +10,11 @@ from crewai.telemetry import Telemetry def cleanup_telemetry(): """Automatically clean up Telemetry singleton between tests.""" Telemetry._instance = None - yield + with patch( + "crewai.telemetry.telemetry.has_user_declined_tracing", + return_value=False, + ): + yield Telemetry._instance = None @@ -32,9 +37,10 @@ def test_telemetry_environment_variables(env_var, value, expected_ready): "OTEL_SDK_DISABLED": "false", "CREWAI_DISABLE_TELEMETRY": "false", "CREWAI_DISABLE_TRACKING": "false", + "CREWAI_TRACING_ENABLED": "", env_var: value, } - with patch.dict(os.environ, clean_env): + with patch.dict(os.environ, clean_env, clear=True): telemetry = Telemetry() assert telemetry.ready is expected_ready @@ -105,3 +111,125 @@ def test_telemetry_otel_sdk_disabled_after_creation(): telemetry._safe_telemetry_operation(mock_operation) mock_operation.assert_not_called() + + +@pytest.mark.telemetry +@pytest.mark.parametrize( + "tracing_value", + ["false", "False", "FALSE", "0"], +) +def test_telemetry_disabled_when_crewai_tracing_enabled_is_false(tracing_value): + """Test that telemetry is disabled when CREWAI_TRACING_ENABLED is explicitly false. + + Regression test for https://github.com/crewAIInc/crewAI/issues/4525 + """ + clean_env = { + "OTEL_SDK_DISABLED": "false", + "CREWAI_DISABLE_TELEMETRY": "false", + "CREWAI_DISABLE_TRACKING": "false", + "CREWAI_TRACING_ENABLED": tracing_value, + } + with patch.dict(os.environ, clean_env): + telemetry = Telemetry() + assert telemetry.ready is False + + +@pytest.mark.telemetry +def test_telemetry_not_disabled_when_crewai_tracing_enabled_unset(): + """Test that telemetry remains enabled when CREWAI_TRACING_ENABLED is not set.""" + clean_env = { + "OTEL_SDK_DISABLED": "false", + "CREWAI_DISABLE_TELEMETRY": "false", + "CREWAI_DISABLE_TRACKING": "false", + } + with patch.dict(os.environ, clean_env, clear=True): + with patch("crewai.telemetry.telemetry.TracerProvider"): + with patch( + "crewai.telemetry.telemetry.has_user_declined_tracing", + return_value=False, + ): + telemetry = Telemetry() + assert telemetry.ready is True + + +@pytest.mark.telemetry +def test_telemetry_disabled_when_user_declined_tracing(): + """Test that telemetry is disabled when user has declined tracing via first-time prompt. + + Regression test for https://github.com/crewAIInc/crewAI/issues/4525 + """ + clean_env = { + "OTEL_SDK_DISABLED": "false", + "CREWAI_DISABLE_TELEMETRY": "false", + "CREWAI_DISABLE_TRACKING": "false", + } + with patch.dict(os.environ, clean_env, clear=True): + with patch( + "crewai.telemetry.telemetry.has_user_declined_tracing", + return_value=True, + ) as mock_declined: + telemetry = Telemetry() + mock_declined.assert_called() + assert telemetry.ready is False + + +@pytest.mark.telemetry +def test_telemetry_operations_blocked_when_crewai_tracing_enabled_false_after_init(): + """Test that telemetry operations are blocked when CREWAI_TRACING_ENABLED=false set after init. + + Regression test for https://github.com/crewAIInc/crewAI/issues/4525 + """ + with patch.dict(os.environ, {}, clear=True): + with patch("crewai.telemetry.telemetry.TracerProvider"): + telemetry = Telemetry() + assert telemetry.ready is True + + mock_operation = MagicMock() + telemetry._safe_telemetry_operation(mock_operation) + mock_operation.assert_called_once() + + mock_operation.reset_mock() + + os.environ["CREWAI_TRACING_ENABLED"] = "false" + + telemetry._safe_telemetry_operation(mock_operation) + mock_operation.assert_not_called() + + +@pytest.mark.telemetry +def test_telemetry_operations_allowed_when_tracing_context_true(): + """Test that telemetry operations are allowed when tracing context var is True.""" + with patch.dict(os.environ, {}, clear=True): + with patch("crewai.telemetry.telemetry.TracerProvider"): + with patch( + "crewai.telemetry.telemetry.has_user_declined_tracing", + return_value=False, + ): + telemetry = Telemetry() + assert telemetry.ready is True + + mock_operation = MagicMock() + + token = _tracing_enabled.set(True) + try: + telemetry._safe_telemetry_operation(mock_operation) + mock_operation.assert_called_once() + finally: + _tracing_enabled.reset(token) + + +@pytest.mark.telemetry +def test_telemetry_operations_allowed_when_tracing_context_none(): + """Test that telemetry operations are allowed when tracing context var is None (default).""" + with patch.dict(os.environ, {}, clear=True): + with patch("crewai.telemetry.telemetry.TracerProvider"): + with patch( + "crewai.telemetry.telemetry.has_user_declined_tracing", + return_value=False, + ): + telemetry = Telemetry() + assert telemetry.ready is True + + mock_operation = MagicMock() + telemetry._safe_telemetry_operation(mock_operation) + mock_operation.assert_called_once()