From 2c73b8d7cb4b305230f81ed54256700f5fe10552 Mon Sep 17 00:00:00 2001 From: Alexander Piskun Date: Thu, 21 May 2026 18:44:22 +0000 Subject: [PATCH] feat(tracking): honor DO_NOT_TRACK and COMFY_NO_TELEMETRY env vars Short-circuits both the consent prompt and the per-event dispatch when either variable is set to a non-empty, non-zero value, per the consoledonottrack.com spec. Suppresses the non-TTY auto-enable path so no anonymous user_id is persisted before consent. Signed-off-by: Alexander Piskun --- comfy_cli/tracking.py | 24 ++++++++ tests/comfy_cli/test_tracking.py | 98 ++++++++++++++++++++++++++++++++ 2 files changed, 122 insertions(+) diff --git a/comfy_cli/tracking.py b/comfy_cli/tracking.py index b5d9db7f..32695a66 100644 --- a/comfy_cli/tracking.py +++ b/comfy_cli/tracking.py @@ -54,6 +54,21 @@ _session_only_tracking = False +def _telemetry_disabled_by_env() -> bool: + """Return True if telemetry is suppressed via environment variable. + + Honors the cross-tool ``DO_NOT_TRACK`` convention + (https://consoledonottrack.com/) and the project-specific + ``COMFY_NO_TELEMETRY``. Per the spec, any value other than empty or + ``"0"`` opts out. + """ + for name in ("DO_NOT_TRACK", "COMFY_NO_TELEMETRY"): + val = os.environ.get(name, "") + if val and val != "0": + return True + return False + + class TelemetryProvider(Protocol): enabled: bool @@ -136,6 +151,8 @@ def track_event(event_name: str, properties: Any = None, *, mixpanel_name: str | ``mixpanel_name``, if supplied, overrides the event name on the Mixpanel pipe only — used to keep legacy Mixpanel event names while PostHog receives the canonical name. """ + if _telemetry_disabled_by_env(): + return if properties is None: properties = {} logging.debug(f"tracking event called with event_name: {event_name} and properties: {properties}") @@ -187,6 +204,13 @@ def wrapper(*args, **kwargs): def prompt_tracking_consent(skip_prompt: bool = False, default_value: bool = False): global _session_only_tracking, user_id + # Env-var opt-out short-circuits everything below: no prompt, no + # auto-enable in non-TTY, no user_id persistence. Per-process only — + # the on-disk consent flag is left untouched so a later run without + # the env var still gets the normal prompt path. + if _telemetry_disabled_by_env(): + return + if _session_only_tracking: return diff --git a/tests/comfy_cli/test_tracking.py b/tests/comfy_cli/test_tracking.py index f4440a1c..44ec4b7a 100644 --- a/tests/comfy_cli/test_tracking.py +++ b/tests/comfy_cli/test_tracking.py @@ -280,3 +280,101 @@ def test_session_only_is_idempotent(self, tracking_module): first_user_id = tracking_module.user_id tracking_module.prompt_tracking_consent() assert tracking_module.user_id == first_user_id + + +class TestEnvVarOptOut: + @pytest.mark.parametrize("env_var", ["DO_NOT_TRACK", "COMFY_NO_TELEMETRY"]) + def test_env_var_blocks_track_event_even_when_config_enabled(self, tracking_module, monkeypatch, env_var): + tracking_module.config_manager.set(constants.CONFIG_KEY_ENABLE_TRACKING, "True") + monkeypatch.setenv(env_var, "1") + tracking_module.track_event("some_event", {"k": "v"}) + tracking_module.provider.track.assert_not_called() + + @pytest.mark.parametrize("env_var", ["DO_NOT_TRACK", "COMFY_NO_TELEMETRY"]) + def test_env_var_blocks_track_event_under_session_only(self, tracking_module, monkeypatch, env_var): + monkeypatch.setenv(env_var, "1") + with patch.object(tracking_module, "_session_only_tracking", True): + tracking_module.track_event("some_event") + tracking_module.provider.track.assert_not_called() + + @pytest.mark.parametrize("falsy", ["", "0"]) + def test_falsy_values_do_not_block(self, tracking_module, monkeypatch, falsy): + tracking_module.config_manager.set(constants.CONFIG_KEY_ENABLE_TRACKING, "True") + monkeypatch.setenv("DO_NOT_TRACK", falsy) + monkeypatch.setenv("COMFY_NO_TELEMETRY", falsy) + tracking_module.track_event("some_event") + tracking_module.provider.track.assert_called_once() + + @pytest.mark.parametrize("env_var", ["DO_NOT_TRACK", "COMFY_NO_TELEMETRY"]) + def test_env_var_short_circuits_consent_prompt(self, tracking_module, monkeypatch, env_var): + monkeypatch.setenv(env_var, "1") + with ( + patch.object(tracking_module.sys.stdin, "isatty", return_value=True), + patch.object(tracking_module.sys.stdout, "isatty", return_value=True), + patch.object(tracking_module.ui, "prompt_confirm_action") as mock_prompt, + ): + tracking_module.prompt_tracking_consent() + mock_prompt.assert_not_called() + assert tracking_module.config_manager.get_bool(constants.CONFIG_KEY_ENABLE_TRACKING) is None + + @pytest.mark.parametrize("env_var", ["DO_NOT_TRACK", "COMFY_NO_TELEMETRY"]) + def test_env_var_blocks_non_tty_auto_enable_and_user_id_persist(self, tracking_module, monkeypatch, env_var): + # Reporter's core concern (issue #462): in CI/Docker the non-TTY + # branch silently persisted a UUID. Env var must skip that path. + monkeypatch.setenv(env_var, "1") + with ( + patch.object(tracking_module.sys.stdin, "isatty", return_value=False), + patch.object(tracking_module.sys.stdout, "isatty", return_value=False), + ): + tracking_module.prompt_tracking_consent() + assert tracking_module._session_only_tracking is False + assert tracking_module.config_manager.get(constants.CONFIG_KEY_USER_ID) is None + + def test_env_var_does_not_overwrite_existing_consent(self, tracking_module, monkeypatch): + # On-disk consent flag must survive an env-var-suppressed run so a + # subsequent invocation without the env var keeps the user's choice. + tracking_module.config_manager.set(constants.CONFIG_KEY_ENABLE_TRACKING, "True") + monkeypatch.setenv("DO_NOT_TRACK", "1") + tracking_module.prompt_tracking_consent() + assert tracking_module.config_manager.get_bool(constants.CONFIG_KEY_ENABLE_TRACKING) is True + + +class TestTelemetryDisabledByEnvHelper: + @pytest.fixture(autouse=True) + def _clear_both(self, monkeypatch): + monkeypatch.delenv("DO_NOT_TRACK", raising=False) + monkeypatch.delenv("COMFY_NO_TELEMETRY", raising=False) + + def test_unset_returns_false(self, tracking_module): + import comfy_cli.tracking as tm + + assert tm._telemetry_disabled_by_env() is False + + @pytest.mark.parametrize("env_var", ["DO_NOT_TRACK", "COMFY_NO_TELEMETRY"]) + @pytest.mark.parametrize( + "value,expected", + [ + # consoledonottrack.com spec: empty or "0" allows tracking; anything else opts out. + ("", False), + ("0", False), + ("1", True), + ("true", True), + ("yes", True), + ("00", True), + ("false", True), + ], + ) + def test_value_semantics(self, tracking_module, monkeypatch, env_var, value, expected): + import comfy_cli.tracking as tm + + monkeypatch.setenv(env_var, value) + assert tm._telemetry_disabled_by_env() is expected + + def test_either_var_alone_is_sufficient(self, tracking_module, monkeypatch): + import comfy_cli.tracking as tm + + monkeypatch.setenv("COMFY_NO_TELEMETRY", "1") + assert tm._telemetry_disabled_by_env() is True + monkeypatch.delenv("COMFY_NO_TELEMETRY") + monkeypatch.setenv("DO_NOT_TRACK", "1") + assert tm._telemetry_disabled_by_env() is True