From 1dfaf3d552af1bbcec9f95ac814dd979d7b10ee1 Mon Sep 17 00:00:00 2001 From: Joao Moura Date: Fri, 26 Jun 2026 00:08:21 -0700 Subject: [PATCH 1/2] feat(cli): track TUI button telemetry --- lib/cli/src/crewai_cli/crew_run_tui.py | 13 ++++++++ lib/cli/tests/test_crew_run_tui.py | 33 ++++++++++++++++++++ lib/crewai-core/src/crewai_core/telemetry.py | 11 +++++++ lib/crewai-core/tests/test_smoke.py | 27 ++++++++++++++++ 4 files changed, 84 insertions(+) diff --git a/lib/cli/src/crewai_cli/crew_run_tui.py b/lib/cli/src/crewai_cli/crew_run_tui.py index 81aae6c470..9a7b47a7f0 100644 --- a/lib/cli/src/crewai_cli/crew_run_tui.py +++ b/lib/cli/src/crewai_cli/crew_run_tui.py @@ -10,6 +10,7 @@ import time from typing import Any, ClassVar, cast +from crewai_core.telemetry import Telemetry from rich.text import Text from textual import work from textual.app import App, ComposeResult @@ -571,6 +572,7 @@ def __init__( self._want_deploy: bool = False self._trace_url: str | None = None self._consent_screen: TraceConsentScreen | None = None + self._telemetry: Telemetry | None = None # ── Layout ────────────────────────────────────────────── @@ -1042,10 +1044,21 @@ def action_deploy_crew(self) -> None: self._unsubscribe() self.exit(self._crew_result) + def _record_tui_button_click(self, button_name: str) -> None: + try: + if self._telemetry is None: + self._telemetry = Telemetry() + self._telemetry.set_tracer() + self._telemetry.tui_button_clicked_span(button_name) + except Exception: # noqa: S110 + pass + def on_button_pressed(self, event: Button.Pressed) -> None: if event.button.id in ("btn-traces", "btn-traces-done"): + self._record_tui_button_click("view_traces") self.action_view_traces() elif event.button.id == "btn-deploy": + self._record_tui_button_click("deploy") self.action_deploy_crew() def _scroll_to_result(self) -> None: diff --git a/lib/cli/tests/test_crew_run_tui.py b/lib/cli/tests/test_crew_run_tui.py index 5c49dabf11..3bbcf3a3c1 100644 --- a/lib/cli/tests/test_crew_run_tui.py +++ b/lib/cli/tests/test_crew_run_tui.py @@ -1,5 +1,7 @@ from datetime import datetime import time +from types import SimpleNamespace +from unittest.mock import Mock import pytest @@ -126,6 +128,37 @@ def login(self) -> None: assert "Deploy failed with exit code 42" in capsys.readouterr().out +def test_view_traces_button_click_records_telemetry(monkeypatch) -> None: + app = CrewRunApp() + app._status = "completed" + app._trace_url = "https://app.crewai.com/traces/test" + app._telemetry = Mock() + opened_urls: list[str] = [] + + monkeypatch.setattr("webbrowser.open", lambda url: opened_urls.append(url)) + + app.on_button_pressed(SimpleNamespace(button=SimpleNamespace(id="btn-traces"))) + + app._telemetry.tui_button_clicked_span.assert_called_once_with("view_traces") + assert opened_urls == ["https://app.crewai.com/traces/test"] + + +def test_deploy_button_click_records_telemetry() -> None: + app = CrewRunApp() + app._status = "completed" + app._crew_result = object() + app._telemetry = Mock() + app._unsubscribe = lambda: None # type: ignore[method-assign] + exits: list[object] = [] + app.exit = lambda result: exits.append(result) # type: ignore[method-assign] + + app.on_button_pressed(SimpleNamespace(button=SimpleNamespace(id="btn-deploy"))) + + app._telemetry.tui_button_clicked_span.assert_called_once_with("deploy") + assert app._want_deploy is True + assert exits == [app._crew_result] + + def test_conversation_turn_done_records_assistant_message() -> None: class RawResult: raw = "hello from the flow" diff --git a/lib/crewai-core/src/crewai_core/telemetry.py b/lib/crewai-core/src/crewai_core/telemetry.py index 08aef9b714..82cb8bd62a 100644 --- a/lib/crewai-core/src/crewai_core/telemetry.py +++ b/lib/crewai-core/src/crewai_core/telemetry.py @@ -249,6 +249,17 @@ def _operation() -> None: self._safe_telemetry_procedure(_operation) + def tui_button_clicked_span(self, button_name: str) -> None: + """Records when a user clicks a button in the CLI TUI.""" + + def _operation() -> None: + tracer = trace.get_tracer("crewai.telemetry") + span = tracer.start_span("TUI Button Clicked") + self._add_attribute(span, "button_name", button_name) + close_span(span) + + self._safe_telemetry_procedure(_operation) + def flow_creation_span(self, flow_name: str) -> None: """Records the creation of a new flow.""" diff --git a/lib/crewai-core/tests/test_smoke.py b/lib/crewai-core/tests/test_smoke.py index 7980cfd1e9..862cd6642d 100644 --- a/lib/crewai-core/tests/test_smoke.py +++ b/lib/crewai-core/tests/test_smoke.py @@ -4,6 +4,7 @@ import os from pathlib import Path +from unittest.mock import Mock from crewai_core import ( constants, @@ -128,3 +129,29 @@ def fail_if_called(provider: object) -> None: assert called is False assert telemetry.trace_set is True + + +def test_core_telemetry_records_tui_button_click( + monkeypatch: pytest.MonkeyPatch, +) -> None: + from crewai_core.telemetry import Telemetry + + Telemetry._instance = None + monkeypatch.delenv("OTEL_SDK_DISABLED", raising=False) + monkeypatch.delenv("CREWAI_DISABLE_TELEMETRY", raising=False) + monkeypatch.delenv("CREWAI_DISABLE_TRACKING", raising=False) + + tracer = Mock() + span = Mock() + tracer.start_span.return_value = span + monkeypatch.setattr( + "crewai_core.telemetry.trace.get_tracer", + lambda _name: tracer, + ) + + telemetry = Telemetry() + telemetry.tui_button_clicked_span("view_traces") + + tracer.start_span.assert_called_once_with("TUI Button Clicked") + span.set_attribute.assert_called_once_with("button_name", "view_traces") + span.end.assert_called_once() From 278663af7993facb5bcba79b550dc8cdef8b5a4c Mon Sep 17 00:00:00 2001 From: Joao Moura Date: Fri, 26 Jun 2026 12:04:49 -0700 Subject: [PATCH 2/2] fix(cli): use feature usage telemetry for TUI buttons --- lib/cli/src/crewai_cli/crew_run_tui.py | 2 +- lib/cli/tests/test_crew_run_tui.py | 4 ++-- lib/crewai-core/src/crewai_core/telemetry.py | 10 ++++++---- lib/crewai-core/tests/test_smoke.py | 8 ++++---- 4 files changed, 13 insertions(+), 11 deletions(-) diff --git a/lib/cli/src/crewai_cli/crew_run_tui.py b/lib/cli/src/crewai_cli/crew_run_tui.py index 9a7b47a7f0..cefb7d1429 100644 --- a/lib/cli/src/crewai_cli/crew_run_tui.py +++ b/lib/cli/src/crewai_cli/crew_run_tui.py @@ -1049,7 +1049,7 @@ def _record_tui_button_click(self, button_name: str) -> None: if self._telemetry is None: self._telemetry = Telemetry() self._telemetry.set_tracer() - self._telemetry.tui_button_clicked_span(button_name) + self._telemetry.feature_usage_span(f"cli_usage:{button_name}") except Exception: # noqa: S110 pass diff --git a/lib/cli/tests/test_crew_run_tui.py b/lib/cli/tests/test_crew_run_tui.py index 3bbcf3a3c1..e41b4ec9b2 100644 --- a/lib/cli/tests/test_crew_run_tui.py +++ b/lib/cli/tests/test_crew_run_tui.py @@ -139,7 +139,7 @@ def test_view_traces_button_click_records_telemetry(monkeypatch) -> None: app.on_button_pressed(SimpleNamespace(button=SimpleNamespace(id="btn-traces"))) - app._telemetry.tui_button_clicked_span.assert_called_once_with("view_traces") + app._telemetry.feature_usage_span.assert_called_once_with("cli_usage:view_traces") assert opened_urls == ["https://app.crewai.com/traces/test"] @@ -154,7 +154,7 @@ def test_deploy_button_click_records_telemetry() -> None: app.on_button_pressed(SimpleNamespace(button=SimpleNamespace(id="btn-deploy"))) - app._telemetry.tui_button_clicked_span.assert_called_once_with("deploy") + app._telemetry.feature_usage_span.assert_called_once_with("cli_usage:deploy") assert app._want_deploy is True assert exits == [app._crew_result] diff --git a/lib/crewai-core/src/crewai_core/telemetry.py b/lib/crewai-core/src/crewai_core/telemetry.py index 82cb8bd62a..026c892435 100644 --- a/lib/crewai-core/src/crewai_core/telemetry.py +++ b/lib/crewai-core/src/crewai_core/telemetry.py @@ -249,13 +249,15 @@ def _operation() -> None: self._safe_telemetry_procedure(_operation) - def tui_button_clicked_span(self, button_name: str) -> None: - """Records when a user clicks a button in the CLI TUI.""" + def feature_usage_span(self, feature: str) -> None: + """Records that a feature was used. One span = one count.""" + from crewai_core.version import get_crewai_version def _operation() -> None: tracer = trace.get_tracer("crewai.telemetry") - span = tracer.start_span("TUI Button Clicked") - self._add_attribute(span, "button_name", button_name) + span = tracer.start_span("Feature Usage") + self._add_attribute(span, "crewai_version", get_crewai_version()) + self._add_attribute(span, "feature", feature) close_span(span) self._safe_telemetry_procedure(_operation) diff --git a/lib/crewai-core/tests/test_smoke.py b/lib/crewai-core/tests/test_smoke.py index 862cd6642d..315162b16b 100644 --- a/lib/crewai-core/tests/test_smoke.py +++ b/lib/crewai-core/tests/test_smoke.py @@ -131,7 +131,7 @@ def fail_if_called(provider: object) -> None: assert telemetry.trace_set is True -def test_core_telemetry_records_tui_button_click( +def test_core_telemetry_records_feature_usage( monkeypatch: pytest.MonkeyPatch, ) -> None: from crewai_core.telemetry import Telemetry @@ -150,8 +150,8 @@ def test_core_telemetry_records_tui_button_click( ) telemetry = Telemetry() - telemetry.tui_button_clicked_span("view_traces") + telemetry.feature_usage_span("cli_usage:view_traces") - tracer.start_span.assert_called_once_with("TUI Button Clicked") - span.set_attribute.assert_called_once_with("button_name", "view_traces") + tracer.start_span.assert_called_once_with("Feature Usage") + span.set_attribute.assert_any_call("feature", "cli_usage:view_traces") span.end.assert_called_once()