feat(telemetry): count deployments from any origin and record where they started (#6974)

Deployments were only countable through two span types that no aggregation
reads, and cli_usage:deploy counted the TUI button rather than deployments.
Emit deploy:created and deploy:pushed alongside the existing spans so a
deployment is countable from the feature-usage aggregation no matter how it
was started, and tag Create Crew Deployment / Start Deployment with
source=cli|tui so the two origins stay distinguishable.

Separately, the TUI's `t` and `d` key bindings dispatch straight to
action_view_traces / action_deploy_crew, which never recorded anything -
only on_button_pressed did. Every keyboard-driven trace view and deploy was
therefore invisible. Move the recording into the actions, which both input
paths funnel through, and past the completed guard so a mid-run keypress
that does nothing is not counted as usage.


Claude-Session: https://claude.ai/code/session_01ASfWmW3RGy4qAQm6s8U9jH

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
João Moura
2026-08-12 12:41:59 -03:00
committed by GitHub
parent 8b646620be
commit 93d7a07422
10 changed files with 282 additions and 20 deletions

View File

@@ -1027,6 +1027,9 @@ FooterKey .footer-key--key {
def action_view_traces(self) -> None:
if self._status != "completed":
return
# Recorded here rather than in on_button_pressed so the `t` key binding
# is counted too, and only once the action can actually do something.
self._record_tui_button_click("view_traces")
if self._trace_url:
import webbrowser
@@ -1115,6 +1118,9 @@ FooterKey .footer-key--key {
def action_deploy_crew(self) -> None:
if self._status != "completed":
return
# Recorded here rather than in on_button_pressed so the `d` key binding
# is counted too, and only once the action can actually do something.
self._record_tui_button_click("deploy")
self._want_deploy = True
self._unsubscribe()
self.exit(self._crew_result)
@@ -1130,10 +1136,8 @@ FooterKey .footer-key--key {
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:

View File

@@ -5,6 +5,7 @@ from urllib.parse import quote
import webbrowser
from crewai_core.plus_api import CreateCrewPayload
from crewai_core.telemetry import DeploySource
from rich.console import Console
from crewai_cli import git
@@ -285,17 +286,23 @@ class DeployCommand(BaseCommand, PlusAPIMixin):
return _deployment_identifier(status_response)
def deploy(self, uuid: str | None = None, skip_validate: bool = False) -> None:
def deploy(
self,
uuid: str | None = None,
skip_validate: bool = False,
source: DeploySource = "cli",
) -> None:
"""
Deploy a crew using either UUID or project name.
Args:
uuid (Optional[str]): The UUID of the crew to deploy.
skip_validate (bool): Skip pre-deploy validation checks.
source (DeploySource): Where the deployment was initiated from.
"""
if not _prepare_project_for_deploy(skip_validate):
return
self._telemetry.start_deployment_span(uuid)
self._telemetry.start_deployment_span(uuid, source=source)
console.print("Starting deployment...", style="bold blue")
repository = self._prepare_git_repository()
remote_repo_url = repository.origin_url() if repository else None
@@ -337,17 +344,23 @@ class DeployCommand(BaseCommand, PlusAPIMixin):
raise ValueError("Deployment status response did not include a uuid")
return str(uuid)
def create_crew(self, confirm: bool = False, skip_validate: bool = False) -> None:
def create_crew(
self,
confirm: bool = False,
skip_validate: bool = False,
source: DeploySource = "cli",
) -> None:
"""
Create a new crew deployment.
Args:
confirm (bool): Whether to skip the interactive confirmation prompt.
skip_validate (bool): Skip pre-deploy validation checks.
source (DeploySource): Where the deployment was initiated from.
"""
if not _prepare_project_for_deploy(skip_validate):
return
self._telemetry.create_crew_deployment_span()
self._telemetry.create_crew_deployment_span(source=source)
console.print("Creating deployment...", style="bold blue")
env_vars = fetch_and_json_env_file()
repository = self._prepare_git_repository()

View File

@@ -512,14 +512,14 @@ def _chain_deploy() -> None:
from crewai_cli.deploy.main import DeployCommand
console.print("\nStarting deployment…\n", style="bold #FF5A50")
DeployCommand().create_crew(confirm=True, skip_validate=True)
DeployCommand().create_crew(confirm=True, skip_validate=True, source="tui")
except AuthenticationRequiredError:
from crewai_cli.authentication.main import AuthenticationCommand
console.print()
AuthenticationCommand().login()
try:
DeployCommand().create_crew(confirm=True, skip_validate=True)
DeployCommand().create_crew(confirm=True, skip_validate=True, source="tui")
except AuthenticationRequiredError:
console.print(
"\nDeploy failed: authentication is still required.\n",

View File

@@ -104,8 +104,8 @@ def test_chain_deploy_skips_validation_after_auth_retry(monkeypatch) -> None:
run_crew._chain_deploy()
assert create_calls == [
{"confirm": True, "skip_validate": True},
{"confirm": True, "skip_validate": True},
{"confirm": True, "skip_validate": True, "source": "tui"},
{"confirm": True, "skip_validate": True, "source": "tui"},
]
assert login_calls == [True]
@@ -131,7 +131,7 @@ def test_chain_deploy_does_not_login_for_deploy_exit(monkeypatch, capsys) -> Non
run_crew._chain_deploy()
assert create_calls == [{"confirm": True, "skip_validate": True}]
assert create_calls == [{"confirm": True, "skip_validate": True, "source": "tui"}]
assert login_calls == []
assert "Deploy failed with exit code 42" in capsys.readouterr().out
@@ -1721,3 +1721,86 @@ async def test_declarative_flow_runs_on_tui() -> None:
assert app._final_output == "flow result"
assert app._crew_result == "flow result"
assert app._flow_steps[0]["status"] == "done"
def test_view_traces_keybinding_records_telemetry(monkeypatch) -> None:
"""The `t` binding reaches the action directly, never on_button_pressed."""
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.action_view_traces()
app._telemetry.feature_usage_span.assert_called_once_with("cli_usage:view_traces")
assert opened_urls == ["https://app.crewai.com/traces/test"]
def test_deploy_keybinding_records_telemetry() -> None:
"""The `d` binding reaches the action directly, never on_button_pressed."""
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.action_deploy_crew()
app._telemetry.feature_usage_span.assert_called_once_with("cli_usage:deploy")
assert app._want_deploy is True
assert exits == [app._crew_result]
def test_view_traces_before_completion_records_nothing() -> None:
"""A keypress mid-run is a no-op, so it must not be counted as usage."""
app = CrewRunApp()
app._status = "running"
app._telemetry = Mock()
app.action_view_traces()
app._telemetry.feature_usage_span.assert_not_called()
def test_deploy_before_completion_records_nothing() -> None:
app = CrewRunApp()
app._status = "running"
app._telemetry = Mock()
app.action_deploy_crew()
app._telemetry.feature_usage_span.assert_not_called()
assert app._want_deploy is False
def test_button_press_records_exactly_once(monkeypatch) -> None:
"""Recording moved into the action; the button must not double-count."""
app = CrewRunApp()
app._status = "completed"
app._trace_url = "https://app.crewai.com/traces/test"
app._telemetry = Mock()
monkeypatch.setattr("webbrowser.open", lambda url: None)
app.on_button_pressed(SimpleNamespace(button=SimpleNamespace(id="btn-traces")))
assert app._telemetry.feature_usage_span.call_count == 1
def test_finished_traces_button_still_records(monkeypatch) -> None:
"""The button's id is swapped to btn-traces-done once a trace URL exists."""
app = CrewRunApp()
app._status = "completed"
app._trace_url = "https://app.crewai.com/traces/test"
app._telemetry = Mock()
monkeypatch.setattr("webbrowser.open", lambda url: None)
app.on_button_pressed(SimpleNamespace(button=SimpleNamespace(id="btn-traces-done")))
app._telemetry.feature_usage_span.assert_called_once_with("cli_usage:view_traces")