Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions cloudsmith_cli/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -97,3 +97,11 @@ def reset_wait_budget():
from cloudsmith_cli.core import telemetry_common

telemetry_common.WaitBudget.shared().reset(1.0)


@pytest.fixture(autouse=True)
def usage_state_dir(monkeypatch, tmp_path_factory):
"""Keep usage telemetry state out of the developer's real config dir."""
path = tmp_path_factory.mktemp("usage-state")
monkeypatch.setattr("cloudsmith_cli.core.usage.state.config_dir", lambda: str(path))
return path
50 changes: 50 additions & 0 deletions cloudsmith_cli/core/tests/test_usage_environment.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import pytest

from cloudsmith_cli.core.usage import environment


@pytest.mark.parametrize(
"env,expected",
[
({"GITHUB_ACTIONS": "true"}, "github_actions"),
({"GITLAB_CI": "true"}, "gitlab"),
({"JENKINS_URL": "http://j"}, "jenkins"),
({"CIRCLECI": "true"}, "circleci"),
({"BUILDKITE": "true"}, "buildkite"),
({"TF_BUILD": "True"}, "azure_pipelines"),
({"BITBUCKET_BUILD_NUMBER": "1"}, "bitbucket"),
({"TEAMCITY_VERSION": "2024"}, "teamcity"),
({"CODEBUILD_BUILD_ID": "x"}, "codebuild"),
({"CI": "1"}, "other"),
({}, None),
],
)
def test_ci_provider_by_presence(env, expected):
assert environment.ci_provider(env) == expected


def test_ephemeral_rule(monkeypatch):
monkeypatch.setattr(environment, "is_container", lambda: False)
assert environment.is_ephemeral(env={"CI": "x"}, channel="pip", state_writable=True)
assert environment.is_ephemeral(env={}, channel="docker", state_writable=True)
assert environment.is_ephemeral(env={}, channel="pip", state_writable=False)
assert not environment.is_ephemeral(env={}, channel="pip", state_writable=True)


def test_non_tty_alone_is_not_ci():
assert environment.persona(ci=False) == "human"
assert environment.persona(ci=True) == "ci"


def test_runtime_properties_truncate_versions(monkeypatch):
monkeypatch.setattr(environment.platform, "system", lambda: "Linux")
monkeypatch.setattr(environment.platform, "release", lambda: "6.8.0-1070-azure")
monkeypatch.setattr(environment.platform, "python_version", lambda: "3.12.7")
monkeypatch.setattr(environment.platform, "machine", lambda: "X86_64")
props = environment.runtime_properties()
assert props == {
"os": "Linux",
"os_version": "6.8",
"python_version": "3.12",
"arch": "x86_64",
}
115 changes: 115 additions & 0 deletions cloudsmith_cli/core/tests/test_usage_event.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
from typing import Any

import pytest

from cloudsmith_cli.core.usage import event
from cloudsmith_cli.core.usage.identity import Identity


def _facts(**overrides):
base: dict[str, Any] = dict(
command="list repos",
flags=["output_format", "page_size"],
flags_typed=["-F", "--page-size"],
envvars=["NO_COLOR"],
aliases_used=["ls"],
output_format="json",
outcome="success",
exit_code=0,
api_status=None,
duration_ms=708,
setting_sources={"workspace": "config_file", "color": "env"},
auth_source="keyring",
api_host=None,
ssl_verify=True,
stdout_tty=False,
stdin_tty=False,
nested=False,
)
base.update(overrides)
return event.RunFacts(**base)


def _build(facts, anonymous=True):
ident = Identity(
"id-1", anonymous=anonymous, first_run=False, persisted=not anonymous
)
return event.build_event(
facts=facts,
identity=ident,
runtime={
"os": "Darwin",
"os_version": "26.3",
"python_version": "3.12",
"arch": "arm64",
},
channel="pip",
environment="production",
cli_version="1.31.0",
ci_provider=None,
sample_rate=100,
internal=False,
timestamp="2026-10-23T12:00:00+00:00",
)


def test_only_declared_properties_are_sent():
declared = {p.name for p in event.PROPERTIES}
props = _build(_facts())["properties"]
for key in props:
assert key in declared or (
key.startswith("source_")
and key[len("source_") :] in event.ALLOWED_SETTINGS
), key


def test_snapshot_of_a_typical_event():
payload = _build(_facts())
assert payload["event"] == "cli command executed"
assert payload["distinct_id"] == "id-1"
props = payload["properties"]
assert props["$process_person_profile"] is False
assert props["$geoip_disable"] is True
assert props["command"] == "list repos"
assert props["flags_typed"] == ["--page-size", "-F"]
assert props["source_workspace"] == "config_file"
assert props["persona"] == "human"
assert "api_status" not in props # None values are dropped


def test_install_id_event_creates_no_person_profile():
# A stable install ID still never creates a person profile.
props = _build(_facts(), anonymous=False)["properties"]
assert props["$process_person_profile"] is False


def test_unknown_setting_or_source_is_dropped():
props = _build(
_facts(setting_sources={"secret_thing": "env", "workspace": "weird"})
)["properties"]
assert not any(k.startswith("source_") for k in props)


def test_wrong_type_is_dropped():
props = _build(_facts(duration_ms="708"))["properties"]
assert "duration_ms" not in props


@pytest.mark.parametrize("name", ["flags", "flags_typed", "envvars", "aliases_used"])
@pytest.mark.parametrize("bad", ["--page-size", None, ["ok", 3], {"a": 1}, 7])
def test_malformed_name_lists_are_dropped_not_split(name, bad):
# A bare string must not be sent one character at a time, and None or
# non-string items must not raise.
props = _build(_facts(**{name: bad}))["properties"]
assert name not in props


def test_name_lists_are_deduplicated_and_sorted_but_aliases_keep_order():
props = _build(
_facts(
flags=("page_size", "output_format", "page_size"),
aliases_used=["ls", "repos"],
)
)["properties"]
assert props["flags"] == ["output_format", "page_size"]
assert props["aliases_used"] == ["ls", "repos"]
75 changes: 75 additions & 0 deletions cloudsmith_cli/core/tests/test_usage_identity.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import uuid

import pytest

from cloudsmith_cli.core.usage import identity, state


def _is_uuid(value):
return str(uuid.UUID(value)) == value


def test_default_is_anonymous_per_run_and_writes_nothing(usage_state_dir):
first = identity.resolve(ephemeral=False, install_id_enabled=False)
second = identity.resolve(ephemeral=False, install_id_enabled=False)
assert first.anonymous and second.anonymous
assert first.distinct_id != second.distinct_id and _is_uuid(first.distinct_id)
assert state.read() == {}


def test_ephemeral_never_persists_even_when_enabled(usage_state_dir):
ident = identity.resolve(ephemeral=True, install_id_enabled=True)
assert ident.anonymous and not ident.persisted
assert "install_id" not in state.read()


def test_install_id_persists_and_first_run_flag(usage_state_dir):
first = identity.resolve(ephemeral=False, install_id_enabled=True)
second = identity.resolve(ephemeral=False, install_id_enabled=True)
assert first.first_run and not second.first_run
assert first.distinct_id == second.distinct_id
assert not first.anonymous and first.persisted
assert state.read()["install_id"] == first.distinct_id


@pytest.mark.parametrize(
"stored", ["hello@example.com", "6F9619FF-8B86-D011-B42D-00CF4FC964FF", 7]
)
def test_a_stored_id_that_is_not_a_uuid_is_replaced(usage_state_dir, stored):
# A hand-edited telemetry.json must not put arbitrary text in distinct_id.
state.write({"install_id": stored, "notice_version": 1})
ident = identity.resolve(ephemeral=False, install_id_enabled=True)
assert _is_uuid(ident.distinct_id) and ident.first_run and ident.persisted
assert state.read()["install_id"] == ident.distinct_id
assert state.read()["notice_version"] == 1


def test_reset_deletes_the_id_and_keeps_other_state(usage_state_dir, monkeypatch):
monkeypatch.setenv("CLOUDSMITH_TELEMETRY_INSTALL_ID", "1")
old = identity.resolve(ephemeral=False, install_id_enabled=True).distinct_id
state.write({"notice_version": 1})
identity.reset()
assert "install_id" not in state.read()
assert state.read()["notice_version"] == 1
again = identity.resolve(ephemeral=False, install_id_enabled=True)
assert again.first_run and again.distinct_id != old


def test_reset_never_mints_an_id(usage_state_dir, monkeypatch):
monkeypatch.setenv("CLOUDSMITH_TELEMETRY_INSTALL_ID", "1")
identity.reset()
assert state.read() == {}


def test_reset_raises_when_delete_fails(usage_state_dir, monkeypatch):
identity.resolve(ephemeral=False, install_id_enabled=True)
monkeypatch.setattr(state, "delete", lambda: False)
with pytest.raises(identity.IdentityResetError):
identity.reset()


def test_reset_raises_when_restoring_other_state_fails(usage_state_dir, monkeypatch):
state.write({"install_id": "x", "notice_version": 1})
monkeypatch.setattr(state, "write", lambda updates: False)
with pytest.raises(identity.IdentityResetError):
identity.reset()
24 changes: 24 additions & 0 deletions cloudsmith_cli/core/tests/test_usage_notice.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
from cloudsmith_cli.core.usage import notice, state


def test_needs_notice_until_current_version_recorded(usage_state_dir):
assert notice.needs_notice() is True
state.write({"notice_version": notice.NOTICE_VERSION})
assert notice.needs_notice() is False
state.write({"notice_version": notice.NOTICE_VERSION - 1})
assert notice.needs_notice() is True


def test_show_prints_to_stderr_only(usage_state_dir, capsys):
assert notice.show() is True
out, err = capsys.readouterr()
assert out == ""
assert "usage data" in err and "CLOUDSMITH_NO_TELEMETRY" in err
# The notice names the install ID and how to see and reset it.
assert "install ID" in err
assert "cloudsmith telemetry id" in err and "cloudsmith telemetry reset" in err
assert state.read()["notice_version"] == notice.NOTICE_VERSION


def test_notice_version_bumped_for_the_install_id():
assert notice.NOTICE_VERSION >= 2
38 changes: 38 additions & 0 deletions cloudsmith_cli/core/tests/test_usage_settings.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import pytest

from cloudsmith_cli.core.usage import settings


def test_no_production_key_and_override_wins():
assert settings.POSTHOG_PROJECT_KEY == ""
assert settings.project_key({}) == ""
assert settings.override_set({}) is False
env = {settings.KEY_ENV: "phc_test", settings.HOST_ENV: "https://eu.i.posthog.com"}
assert settings.project_key(env) == "phc_test"
assert settings.override_set(env) is True


def test_flags_parse_truthy_values_only():
assert settings.log_mode({settings.MODE_ENV: "log"}) is True
assert settings.log_mode({settings.MODE_ENV: "1"}) is False
assert settings.internal({settings.INTERNAL_ENV: "true"}) is True
assert settings.internal({settings.INTERNAL_ENV: "0"}) is False
assert settings.install_id_enabled({settings.INSTALL_ID_ENV: "yes"}) is True


@pytest.mark.parametrize("value", ["", "1", "true", "yes", "anything"])
def test_install_id_is_on_by_default(value):
env = {settings.INSTALL_ID_ENV: value} if value else {}
assert settings.install_id_enabled(env) is True


@pytest.mark.parametrize("value", ["0", "false", "no", "off", " FALSE "])
def test_install_id_off_switch(value):
assert settings.install_id_enabled({settings.INSTALL_ID_ENV: value}) is False


def test_ci_sample_rate_defaults_to_100_and_clamps():
assert settings.ci_sample_rate({}) == 100
assert settings.ci_sample_rate({settings.SAMPLE_RATE_ENV: "10"}) == 10
assert settings.ci_sample_rate({settings.SAMPLE_RATE_ENV: "500"}) == 100
assert settings.ci_sample_rate({settings.SAMPLE_RATE_ENV: "x"}) == 100
Loading
Loading