From 030f6d6c438073f52cb137bf34b7cb855cc8b627 Mon Sep 17 00:00:00 2001 From: Greyson LaLonde Date: Wed, 4 Mar 2026 00:45:09 -0500 Subject: [PATCH] fix: use anon id for ephemeral traces --- lib/crewai/src/crewai/cli/plus_api.py | 9 +- lib/crewai/src/crewai/cli/tools/main.py | 5 +- .../listeners/tracing/trace_batch_manager.py | 3 +- lib/crewai/tests/cli/test_plus_api.py | 16 +++- lib/crewai/tests/tracing/test_tracing.py | 84 +++++++++++++++++++ 5 files changed, 111 insertions(+), 6 deletions(-) diff --git a/lib/crewai/src/crewai/cli/plus_api.py b/lib/crewai/src/crewai/cli/plus_api.py index 2658c6b79..e32e5220d 100644 --- a/lib/crewai/src/crewai/cli/plus_api.py +++ b/lib/crewai/src/crewai/cli/plus_api.py @@ -49,8 +49,13 @@ class PlusAPI: with httpx.Client(trust_env=False, verify=verify) as client: return client.request(method, url, headers=self.headers, **kwargs) - def login_to_tool_repository(self) -> httpx.Response: - return self._make_request("POST", f"{self.TOOLS_RESOURCE}/login") + def login_to_tool_repository( + self, user_identifier: str | None = None + ) -> httpx.Response: + payload = {} + if user_identifier: + payload["user_identifier"] = user_identifier + return self._make_request("POST", f"{self.TOOLS_RESOURCE}/login", json=payload) def get_tool(self, handle: str) -> httpx.Response: return self._make_request("GET", f"{self.TOOLS_RESOURCE}/{handle}") diff --git a/lib/crewai/src/crewai/cli/tools/main.py b/lib/crewai/src/crewai/cli/tools/main.py index e2dd21dde..0a9f68af0 100644 --- a/lib/crewai/src/crewai/cli/tools/main.py +++ b/lib/crewai/src/crewai/cli/tools/main.py @@ -23,6 +23,7 @@ from crewai.cli.utils import ( tree_copy, tree_find_and_replace, ) +from crewai.events.listeners.tracing.utils import get_user_id console = Console() @@ -169,7 +170,9 @@ class ToolCommand(BaseCommand, PlusAPIMixin): console.print(f"Successfully installed {handle}", style="bold green") def login(self) -> None: - login_response = self.plus_api_client.login_to_tool_repository() + login_response = self.plus_api_client.login_to_tool_repository( + user_identifier=get_user_id() + ) if login_response.status_code != 200: console.print( diff --git a/lib/crewai/src/crewai/events/listeners/tracing/trace_batch_manager.py b/lib/crewai/src/crewai/events/listeners/tracing/trace_batch_manager.py index c789f7e32..da25792fb 100644 --- a/lib/crewai/src/crewai/events/listeners/tracing/trace_batch_manager.py +++ b/lib/crewai/src/crewai/events/listeners/tracing/trace_batch_manager.py @@ -15,6 +15,7 @@ from crewai.cli.plus_api import PlusAPI from crewai.cli.version import get_crewai_version from crewai.events.listeners.tracing.types import TraceEvent from crewai.events.listeners.tracing.utils import ( + get_user_id, is_tracing_enabled_in_context, should_auto_collect_first_time_traces, ) @@ -120,7 +121,6 @@ class TraceBatchManager: payload = { "trace_id": self.current_batch.batch_id, "execution_type": execution_metadata.get("execution_type", "crew"), - "user_identifier": execution_metadata.get("user_context", None), "execution_context": { "crew_fingerprint": execution_metadata.get("crew_fingerprint"), "crew_name": execution_metadata.get("crew_name", None), @@ -140,6 +140,7 @@ class TraceBatchManager: } if use_ephemeral: payload["ephemeral_trace_id"] = self.current_batch.batch_id + payload["user_identifier"] = get_user_id() response = ( self.plus_api.initialize_ephemeral_trace_batch(payload) diff --git a/lib/crewai/tests/cli/test_plus_api.py b/lib/crewai/tests/cli/test_plus_api.py index 94728db20..95a322e21 100644 --- a/lib/crewai/tests/cli/test_plus_api.py +++ b/lib/crewai/tests/cli/test_plus_api.py @@ -28,7 +28,19 @@ class TestPlusAPI(unittest.TestCase): response = self.api.login_to_tool_repository() mock_make_request.assert_called_once_with( - "POST", "/crewai_plus/api/v1/tools/login" + "POST", "/crewai_plus/api/v1/tools/login", json={} + ) + self.assertEqual(response, mock_response) + + @patch("crewai.cli.plus_api.PlusAPI._make_request") + def test_login_to_tool_repository_with_user_identifier(self, mock_make_request): + mock_response = MagicMock() + mock_make_request.return_value = mock_response + + response = self.api.login_to_tool_repository(user_identifier="test-hash-123") + + mock_make_request.assert_called_once_with( + "POST", "/crewai_plus/api/v1/tools/login", json={"user_identifier": "test-hash-123"} ) self.assertEqual(response, mock_response) @@ -67,7 +79,7 @@ class TestPlusAPI(unittest.TestCase): response = self.api.login_to_tool_repository() self.assert_request_with_org_id( - mock_client_instance, "POST", "/crewai_plus/api/v1/tools/login" + mock_client_instance, "POST", "/crewai_plus/api/v1/tools/login", json={} ) self.assertEqual(response, mock_response) diff --git a/lib/crewai/tests/tracing/test_tracing.py b/lib/crewai/tests/tracing/test_tracing.py index 555446b26..ba49a37c8 100644 --- a/lib/crewai/tests/tracing/test_tracing.py +++ b/lib/crewai/tests/tracing/test_tracing.py @@ -840,3 +840,87 @@ class TestTraceListenerSetup: mock_mark_failed.assert_called_once_with( "test_batch_id_12345", "Internal Server Error" ) + + def test_ephemeral_batch_includes_anon_id(self): + """Test that ephemeral batch initialization sends anon_id from get_user_id()""" + fake_user_id = "abc123def456" + + with ( + patch( + "crewai.events.listeners.tracing.trace_batch_manager.is_tracing_enabled_in_context", + return_value=True, + ), + patch( + "crewai.events.listeners.tracing.trace_batch_manager.get_user_id", + return_value=fake_user_id, + ), + patch( + "crewai.events.listeners.tracing.trace_batch_manager.should_auto_collect_first_time_traces", + return_value=False, + ), + ): + batch_manager = TraceBatchManager() + + mock_response = MagicMock( + status_code=201, + json=MagicMock(return_value={ + "ephemeral_trace_id": "test-trace-id", + "access_code": "TRACE-abc123", + }), + ) + + with patch.object( + batch_manager.plus_api, + "initialize_ephemeral_trace_batch", + return_value=mock_response, + ) as mock_init: + batch_manager.initialize_batch( + user_context={"privacy_level": "standard"}, + execution_metadata={ + "execution_type": "crew", + "crew_name": "test_crew", + }, + use_ephemeral=True, + ) + + mock_init.assert_called_once() + payload = mock_init.call_args[0][0] + assert payload["user_identifier"] == fake_user_id + assert "ephemeral_trace_id" in payload + + def test_non_ephemeral_batch_does_not_include_anon_id(self): + """Test that non-ephemeral batch initialization does not send anon_id""" + with ( + patch( + "crewai.events.listeners.tracing.trace_batch_manager.is_tracing_enabled_in_context", + return_value=True, + ), + patch( + "crewai.events.listeners.tracing.trace_batch_manager.should_auto_collect_first_time_traces", + return_value=False, + ), + ): + batch_manager = TraceBatchManager() + + mock_response = MagicMock( + status_code=201, + json=MagicMock(return_value={"trace_id": "test-trace-id"}), + ) + + with patch.object( + batch_manager.plus_api, + "initialize_trace_batch", + return_value=mock_response, + ) as mock_init: + batch_manager.initialize_batch( + user_context={"privacy_level": "standard"}, + execution_metadata={ + "execution_type": "crew", + "crew_name": "test_crew", + }, + use_ephemeral=False, + ) + + mock_init.assert_called_once() + payload = mock_init.call_args[0][0] + assert "user_identifier" not in payload