diff --git a/lib/crewai/src/crewai/events/listeners/tracing/first_time_trace_handler.py b/lib/crewai/src/crewai/events/listeners/tracing/first_time_trace_handler.py index e3bb7c0c3..8b171ea99 100644 --- a/lib/crewai/src/crewai/events/listeners/tracing/first_time_trace_handler.py +++ b/lib/crewai/src/crewai/events/listeners/tracing/first_time_trace_handler.py @@ -1,6 +1,7 @@ import logging import uuid import webbrowser +from datetime import datetime, timezone from rich.console import Console from rich.panel import Panel @@ -105,11 +106,22 @@ class FirstTimeTraceHandler: if not self.batch_manager.trace_batch_id: self._gracefully_fail("Backend batch creation failed, cannot send events.") + self._reset_batch_state() return self.batch_manager.backend_initialized = True + # Capture values before send/finalize consume them events_count = len(self.batch_manager.event_buffer) + batch_id = self.batch_manager.trace_batch_id + # Read duration non-destructively — _finalize_backend_batch will consume it + start_time = self.batch_manager.execution_start_times.get("execution") + duration_ms = ( + int((datetime.now(timezone.utc) - start_time).total_seconds() * 1000) + if start_time + else 0 + ) + if self.batch_manager.event_buffer: send_status = self.batch_manager._send_events_to_backend() if send_status == 500 and self.batch_manager.trace_batch_id: @@ -117,25 +129,20 @@ class FirstTimeTraceHandler: self.batch_manager.trace_batch_id, "Error sending events to backend", ) + self._reset_batch_state() return self.batch_manager._finalize_backend_batch(events_count) self.ephemeral_url = self.batch_manager.ephemeral_trace_url - # Clean up batch state (mirrors finalize_batch cleanup) - self.batch_manager.batch_owner_type = None - self.batch_manager.batch_owner_id = None - self.batch_manager.current_batch = None - self.batch_manager.event_buffer.clear() - self.batch_manager.trace_batch_id = None - self.batch_manager.is_current_batch_ephemeral = False - self.batch_manager._cleanup_batch_data() - if not self.ephemeral_url: - self._show_local_trace_message() + self._show_local_trace_message(events_count, duration_ms, batch_id) + + self._reset_batch_state() except Exception as e: self._gracefully_fail(f"Backend initialization failed: {e}") + self._reset_batch_state() def _display_ephemeral_trace_link(self): """Display the ephemeral trace link to the user and automatically open browser.""" @@ -206,6 +213,19 @@ To enable tracing later, do any one of these: console.print(panel) console.print() + def _reset_batch_state(self): + """Reset batch manager state to allow future executions to re-initialize.""" + if not self.batch_manager: + return + self.batch_manager.batch_owner_type = None + self.batch_manager.batch_owner_id = None + self.batch_manager.current_batch = None + self.batch_manager.event_buffer.clear() + self.batch_manager.trace_batch_id = None + self.batch_manager.is_current_batch_ephemeral = False + self.batch_manager.backend_initialized = False + self.batch_manager._cleanup_batch_data() + def _gracefully_fail(self, error_message: str): """Handle errors gracefully without disrupting user experience.""" console = Console() @@ -213,7 +233,7 @@ To enable tracing later, do any one of these: logger.debug(f"First-time trace error: {error_message}") - def _show_local_trace_message(self): + def _show_local_trace_message(self, events_count: int = 0, duration_ms: int = 0, batch_id: str | None = None): """Show message when traces were collected locally but couldn't be uploaded.""" console = Console() @@ -221,9 +241,9 @@ To enable tracing later, do any one of these: 📊 Your execution traces were collected locally! Unfortunately, we couldn't upload them to the server right now, but here's what we captured: -• {len(self.batch_manager.event_buffer)} trace events -• Execution duration: {self.batch_manager.calculate_duration("execution")}ms -• Batch ID: {self.batch_manager.trace_batch_id} +• {events_count} trace events +• Execution duration: {duration_ms}ms +• Batch ID: {batch_id} ✅ Tracing has been enabled for future runs! Your preference has been saved. Future Crew/Flow executions will automatically collect traces. diff --git a/lib/crewai/tests/tracing/test_tracing.py b/lib/crewai/tests/tracing/test_tracing.py index affe558d2..171808628 100644 --- a/lib/crewai/tests/tracing/test_tracing.py +++ b/lib/crewai/tests/tracing/test_tracing.py @@ -1242,7 +1242,7 @@ class TestFirstTimeHandlerBackendInitGuard: return handler, bm def test_backend_initialized_true_on_success(self): - """backend_initialized is True and events are sent when batch creation succeeds.""" + """Events are sent when batch creation succeeds, then state is cleaned up.""" handler, bm = self._make_handler_with_manager() server_id = "server-id-abc" @@ -1252,11 +1252,11 @@ class TestFirstTimeHandlerBackendInitGuard: ) mock_send_response = MagicMock(status_code=200) - trace_batch_id_after_init = None + trace_batch_id_during_send = None def capture_send(*args, **kwargs): - nonlocal trace_batch_id_after_init - trace_batch_id_after_init = bm.trace_batch_id + nonlocal trace_batch_id_during_send + trace_batch_id_during_send = bm.trace_batch_id return mock_send_response with ( @@ -1279,8 +1279,12 @@ class TestFirstTimeHandlerBackendInitGuard: bm.event_buffer = [MagicMock(to_dict=MagicMock(return_value={}))] handler._initialize_backend_and_send_events() - assert bm.backend_initialized is True - assert trace_batch_id_after_init == server_id + # trace_batch_id was set correctly during send + assert trace_batch_id_during_send == server_id + # State cleaned up after completion (singleton reuse) + assert bm.backend_initialized is False + assert bm.trace_batch_id is None + assert bm.current_batch is None def test_backend_initialized_false_on_failure(self): """backend_initialized stays False and events are NOT sent when batch creation fails."""