From e2e9e3a1fad9f8fa41966a86d997454e24283bf1 Mon Sep 17 00:00:00 2001 From: Lucain Pouget Date: Thu, 2 Oct 2025 18:27:55 +0200 Subject: [PATCH 1/2] Send CLI telemetry --- src/huggingface_hub/cli/_cli_utils.py | 30 +++++++++++++++++++++++++++ src/huggingface_hub/cli/hf.py | 1 - 2 files changed, 30 insertions(+), 1 deletion(-) diff --git a/src/huggingface_hub/cli/_cli_utils.py b/src/huggingface_hub/cli/_cli_utils.py index 2cd08d3416..78ee5bf20b 100644 --- a/src/huggingface_hub/cli/_cli_utils.py +++ b/src/huggingface_hub/cli/_cli_utils.py @@ -22,6 +22,7 @@ from huggingface_hub import __version__ from huggingface_hub.hf_api import HfApi +from huggingface_hub.utils import send_telemetry class ANSI: @@ -89,6 +90,34 @@ def list_commands(self, ctx: click.Context) -> list[str]: # type: ignore[name-d return sorted(self.commands.keys()) +def telemetry_callback(ctx: typer.Context): + """Send telemetry for each command executed. + + Only the command is logged, not its arguments or options. + Telemetry is anonymous (no user information or token is sent). + Telemetry can be disabled by setting the `HF_HUB_DISABLE_TELEMETRY` environment variable. + """ + if not ( + # Typer groups are called in cascade, so we only want to log once the subcommand is actually a command and + # not a group. + isinstance(ctx.command, click.core.Group) + and isinstance(ctx.command.commands.get(ctx.invoked_subcommand), click.core.Group) + ): + # Read full command path in reverse order to build topic + topic_parts = [] + if ctx.invoked_subcommand: + topic_parts.append(ctx.invoked_subcommand) + while ctx: + if ctx.info_name: + topic_parts.append(ctx.info_name) + ctx = ctx.parent # type: ignore + + # e.g. "hf auth list" => "hf/auth/list" + # "hf download openai-community/gpt2" => "hf/download" + topic = "/".join(reversed(topic_parts)) + send_telemetry(topic=topic, library_name="hf", library_version=__version__) + + def typer_factory(help: str) -> typer.Typer: return typer.Typer( help=help, @@ -96,6 +125,7 @@ def typer_factory(help: str) -> typer.Typer: rich_markup_mode=None, no_args_is_help=True, cls=AlphabeticalMixedGroup, + callback=telemetry_callback, ) diff --git a/src/huggingface_hub/cli/hf.py b/src/huggingface_hub/cli/hf.py index bb8b3b80d0..f6fb9f9bbb 100644 --- a/src/huggingface_hub/cli/hf.py +++ b/src/huggingface_hub/cli/hf.py @@ -12,7 +12,6 @@ # See the License for the specific language governing permissions and # limitations under the License. - from huggingface_hub.cli._cli_utils import typer_factory from huggingface_hub.cli.auth import auth_cli from huggingface_hub.cli.cache import cache_cli From af41bf02d35e53f4e8d38190b865959c295d7423 Mon Sep 17 00:00:00 2001 From: Lucain Pouget Date: Fri, 3 Oct 2025 10:26:27 +0200 Subject: [PATCH 2/2] ignore --- src/huggingface_hub/cli/_cli_utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/huggingface_hub/cli/_cli_utils.py b/src/huggingface_hub/cli/_cli_utils.py index 78ee5bf20b..1194d30554 100644 --- a/src/huggingface_hub/cli/_cli_utils.py +++ b/src/huggingface_hub/cli/_cli_utils.py @@ -101,7 +101,7 @@ def telemetry_callback(ctx: typer.Context): # Typer groups are called in cascade, so we only want to log once the subcommand is actually a command and # not a group. isinstance(ctx.command, click.core.Group) - and isinstance(ctx.command.commands.get(ctx.invoked_subcommand), click.core.Group) + and isinstance(ctx.command.commands.get(ctx.invoked_subcommand), click.core.Group) # type: ignore ): # Read full command path in reverse order to build topic topic_parts = []