mirror of
https://github.com/crewAIInc/crewAI.git
synced 2026-08-13 01:38:41 +00:00
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:
@@ -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:
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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")
|
||||
|
||||
Reference in New Issue
Block a user