feat(telemetry): track checkpoint runtime and CLI usage (#7348)

* feat(cli): track checkpoint command and TUI usage

* feat(telemetry): track runtime checkpoint operations

* fix(telemetry): count prune usage after argument validation

* style(cli): format checkpoint prune command

---------

Co-authored-by: Vidit Ostwal <110953813+Vidit-Ostwal@users.noreply.github.com>
This commit is contained in:
Lorenze Jay
2026-09-09 09:43:11 -07:00
committed by GitHub
parent a53ecc17f1
commit b0fd0a9fa3
6 changed files with 344 additions and 7 deletions

View File

@@ -8,11 +8,27 @@ import json
import os
import re
import sqlite3
from typing import Any
from typing import Any, Literal
import click
def _record_checkpoint_usage(
action: Literal[
"list", "info", "resume", "diff", "prune", "tui", "tui_resume", "tui_fork"
],
) -> None:
"""Count a CLI action without recording checkpoint data or blocking execution."""
try:
from crewai_core.telemetry import Telemetry
telemetry = Telemetry()
telemetry.set_tracer()
telemetry.feature_usage_span(f"cli_usage:checkpoint_{action}")
except Exception: # noqa: S110 - telemetry must never break a command
pass
_PLACEHOLDER_RE = re.compile(r"\{([A-Za-z_][A-Za-z0-9_\-]*)}")
@@ -696,6 +712,7 @@ def prune_checkpoints(
return
duration: timedelta | None = _parse_duration(older_than) if older_than else None
_record_checkpoint_usage("prune")
deleted: int
if _is_sqlite(location):

View File

@@ -26,6 +26,7 @@ from crewai_cli.checkpoint_cli import (
_is_sqlite,
_list_json,
_list_sqlite,
_record_checkpoint_usage,
)
@@ -699,7 +700,7 @@ class CheckpointTUI(App[_TuiResult]):
if event.node.data is not None:
await self._show_detail(event.node.data)
def _exit_with_action(self, action: str) -> None:
def _exit_with_action(self, action: Literal["resume", "fork"]) -> None:
if self._selected_entry is None:
self.notify("No checkpoint selected", severity="warning")
return
@@ -709,6 +710,7 @@ class CheckpointTUI(App[_TuiResult]):
etype = self._detect_entity_type(self._selected_entry)
name = self._selected_entry.get("name", "")[:30]
self.notify(f"{action.title()}: {name}")
_record_checkpoint_usage("tui_resume" if action == "resume" else "tui_fork")
self.exit((loc, action, inputs, overrides, etype))
def action_resume(self) -> None:

View File

@@ -1268,7 +1268,7 @@ def traces_status() -> None:
@click.pass_context
def checkpoint(ctx: click.Context, location: str) -> None:
"""Browse and inspect checkpoints. Launches a TUI when called without a subcommand."""
from crewai_cli.checkpoint_cli import _detect_location
from crewai_cli.checkpoint_cli import _detect_location, _record_checkpoint_usage
location = _detect_location(location)
ctx.ensure_object(dict)
@@ -1276,6 +1276,7 @@ def checkpoint(ctx: click.Context, location: str) -> None:
if ctx.invoked_subcommand is None:
from crewai_cli.checkpoint_tui import run_checkpoint_tui
_record_checkpoint_usage("tui")
run_checkpoint_tui(location)
@@ -1283,8 +1284,13 @@ def checkpoint(ctx: click.Context, location: str) -> None:
@click.argument("location", default="./.checkpoints")
def checkpoint_list(location: str) -> None:
"""List checkpoints in a directory."""
from crewai_cli.checkpoint_cli import _detect_location, list_checkpoints
from crewai_cli.checkpoint_cli import (
_detect_location,
_record_checkpoint_usage,
list_checkpoints,
)
_record_checkpoint_usage("list")
list_checkpoints(_detect_location(location))
@@ -1292,8 +1298,13 @@ def checkpoint_list(location: str) -> None:
@click.argument("path", default="./.checkpoints")
def checkpoint_info(path: str) -> None:
"""Show details of a checkpoint. Pass a file or directory for latest."""
from crewai_cli.checkpoint_cli import _detect_location, info_checkpoint
from crewai_cli.checkpoint_cli import (
_detect_location,
_record_checkpoint_usage,
info_checkpoint,
)
_record_checkpoint_usage("info")
info_checkpoint(_detect_location(path))
@@ -1302,8 +1313,9 @@ def checkpoint_info(path: str) -> None:
@click.pass_context
def checkpoint_resume(ctx: click.Context, checkpoint_id: str | None) -> None:
"""Resume from a checkpoint. Defaults to the most recent."""
from crewai_cli.checkpoint_cli import resume_checkpoint
from crewai_cli.checkpoint_cli import _record_checkpoint_usage, resume_checkpoint
_record_checkpoint_usage("resume")
resume_checkpoint(ctx.obj["location"], checkpoint_id)
@@ -1313,8 +1325,9 @@ def checkpoint_resume(ctx: click.Context, checkpoint_id: str | None) -> None:
@click.pass_context
def checkpoint_diff(ctx: click.Context, id1: str, id2: str) -> None:
"""Compare two checkpoints side-by-side."""
from crewai_cli.checkpoint_cli import diff_checkpoints
from crewai_cli.checkpoint_cli import _record_checkpoint_usage, diff_checkpoints
_record_checkpoint_usage("diff")
diff_checkpoints(ctx.obj["location"], id1, id2)

View File

@@ -0,0 +1,142 @@
"""Checkpoint CLI usage is counted once, without including checkpoint data."""
from unittest.mock import MagicMock, patch
from click.testing import CliRunner
import pytest
from crewai_cli.checkpoint_cli import _record_checkpoint_usage
from crewai_cli.checkpoint_tui import CheckpointTUI
from crewai_cli.cli import crewai
from crewai_core.telemetry import Telemetry
@pytest.mark.parametrize(
("args", "target", "action"),
[
([], "checkpoint_tui.run_checkpoint_tui", "tui"),
(["list", "/private/checkpoints"], "checkpoint_cli.list_checkpoints", "list"),
(
["info", "/private/checkpoint.json"],
"checkpoint_cli.info_checkpoint",
"info",
),
(["resume", "private-id"], "checkpoint_cli.resume_checkpoint", "resume"),
(
["diff", "private-id-1", "private-id-2"],
"checkpoint_cli.diff_checkpoints",
"diff",
),
],
)
def test_checkpoint_command_usage(args, target, action):
with (
patch("crewai_core.telemetry.Telemetry") as telemetry,
patch(f"crewai_cli.{target}") as operation,
):
result = CliRunner().invoke(crewai, ["checkpoint", *args])
assert result.exit_code == 0, result.output
operation.assert_called_once()
telemetry.return_value.feature_usage_span.assert_called_once_with(
f"cli_usage:checkpoint_{action}"
)
@pytest.mark.parametrize("args", [["--help"], ["list", "--help"], ["diff"]])
def test_help_and_invalid_arguments_are_not_counted(args):
with patch("crewai_core.telemetry.Telemetry") as telemetry:
CliRunner().invoke(crewai, ["checkpoint", *args])
telemetry.assert_not_called()
@pytest.mark.parametrize(
"failure", ["initialization", "set_tracer", "feature_usage_span"]
)
def test_telemetry_failure_does_not_block_command(failure):
with (
patch("crewai_core.telemetry.Telemetry") as telemetry,
patch("crewai_cli.checkpoint_cli.list_checkpoints") as operation,
):
if failure == "initialization":
telemetry.side_effect = RuntimeError("telemetry unavailable")
else:
getattr(telemetry.return_value, failure).side_effect = RuntimeError(
"unavailable"
)
result = CliRunner().invoke(crewai, ["checkpoint", "list"])
assert result.exit_code == 0, result.output
operation.assert_called_once()
@pytest.mark.parametrize(
"flag", ["OTEL_SDK_DISABLED", "CREWAI_DISABLE_TELEMETRY", "CREWAI_DISABLE_TRACKING"]
)
def test_checkpoint_usage_respects_telemetry_opt_out(monkeypatch, flag):
monkeypatch.setenv(flag, "true")
monkeypatch.setattr(Telemetry, "_instance", None)
telemetry = Telemetry()
telemetry.provider = MagicMock()
telemetry.ready = True
_record_checkpoint_usage("list")
telemetry.provider.get_tracer.assert_not_called()
@pytest.mark.asyncio
@pytest.mark.parametrize("action", ["resume", "fork"])
@pytest.mark.parametrize("selected", [False, True])
async def test_tui_counts_only_actions_with_a_selected_checkpoint(action, selected):
app = CheckpointTUI(location="/nonexistent/checkpoints")
with (
patch("crewai_core.telemetry.Telemetry") as telemetry,
patch.object(app, "_collect_inputs", return_value={}),
patch.object(app, "_collect_task_overrides", return_value={}),
patch.object(app, "_resolve_location", return_value="/private/checkpoint.json"),
patch.object(app, "_detect_entity_type", return_value="crew"),
patch.object(app, "exit") as exit_app,
):
async with app.run_test():
app._selected_entry = {"name": "private-id"} if selected else None
getattr(app, f"action_{action}")()
if selected:
telemetry.return_value.feature_usage_span.assert_called_once_with(
f"cli_usage:checkpoint_tui_{action}"
)
exit_app.assert_called_once()
else:
telemetry.assert_not_called()
exit_app.assert_not_called()
@pytest.mark.parametrize("dry_run", [False, True])
@pytest.mark.parametrize("retention", [[], ["--older-than", "invalid"]])
def test_invalid_prune_is_not_counted(tmp_path, dry_run, retention):
args = ["checkpoint", "--location", str(tmp_path), "prune", *retention]
if dry_run:
args.append("--dry-run")
with patch("crewai_core.telemetry.Telemetry") as telemetry:
result = CliRunner().invoke(crewai, args)
if retention:
assert result.exit_code == 2
assert "Invalid duration" in result.output
else:
assert "Specify --keep N and/or --older-than" in result.output
telemetry.assert_not_called()
@pytest.mark.parametrize("dry_run", [False, True])
@pytest.mark.parametrize("retention", [["--keep", "2"], ["--older-than", "7d"]])
def test_valid_prune_is_counted_once(tmp_path, dry_run, retention):
args = ["checkpoint", "--location", str(tmp_path), "prune", *retention]
if dry_run:
args.append("--dry-run")
with patch("crewai_core.telemetry.Telemetry") as telemetry:
result = CliRunner().invoke(crewai, args)
assert result.exit_code == 0, result.output
telemetry.return_value.feature_usage_span.assert_called_once_with(
"cli_usage:checkpoint_prune"
)