From 2695e5d705a26ebe13be1353546235993fa15048 Mon Sep 17 00:00:00 2001 From: Louis Choquel Date: Sun, 11 Oct 2026 22:43:25 +0200 Subject: [PATCH 1/3] Declare cocode as the usage-statistics host before it boots pipelex cocode's CLI sets PIPELEX_USAGE_STATS_HOST=cocode, unless the environment already sets it, right before Pipelex.make, so the anonymous usage statistics the embedded pipelex runtime sends name cocode as their host. The variable is the route pipelex documents for an embedder and works with any pinned pipelex: a release that sends no statistics ignores it. A unit test checks the declaration is in place when pipelex boots, and a docs page says cocode reports through pipelex and how to turn the statistics off. Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 3 +++ cocode/cli/main.py | 3 +++ cocode/usage_stats.py | 23 +++++++++++++++++++++ docs/pages/usage-statistics.md | 25 +++++++++++++++++++++++ mkdocs.yml | 1 + tests/unit/test_usage_stats.py | 37 ++++++++++++++++++++++++++++++++++ 6 files changed, 92 insertions(+) create mode 100644 cocode/usage_stats.py create mode 100644 docs/pages/usage-statistics.md create mode 100644 tests/unit/test_usage_stats.py diff --git a/CHANGELOG.md b/CHANGELOG.md index be911ef..deeb297 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,9 @@ ## [Unreleased] +### Added +- **Usage statistics:** cocode now names itself as the `host` of the anonymous usage statistics sent by the Pipelex runtime it embeds, by setting `PIPELEX_USAGE_STATS_HOST=cocode` before it boots Pipelex unless the environment already sets it. cocode sends no statistics of its own, and a Pipelex release that sends none ignores the variable. The new Usage Statistics page in the docs says what this means and how to turn the statistics off. + ### Fixed - **Docs:** `CLI_README.md`'s example of extracting Python files from the cookbook now reads its `recipes` directory. The cookbook's redesign removed the `examples` directory it pointed at, so the example extracted nothing. - **CI:** Bumped the Sigstore signing action in the publish workflow from `v3.0.0` to `v3.5.0`, SHA-pinned as `790bc6befb9d733738f18d8f895854b453640ec9`. The sigstore-python bundled with `v3.0.0` predates the Sigstore TUF trust-root rotation, so the "Sign the dists with Sigstore" step had started failing deterministically with `tuf.api.exceptions.UnsignedMetadataError: root was signed by 0/3 keys`. That step sits in the GitHub-release job, so the failure mode was a version published to PyPI with no matching GitHub release or tag. diff --git a/cocode/cli/main.py b/cocode/cli/main.py index 80fcd2c..3464d10 100644 --- a/cocode/cli/main.py +++ b/cocode/cli/main.py @@ -20,6 +20,7 @@ from cocode.common import PIPELINE_LIBRARY_DIRS from cocode.github.github_cli import github_app from cocode.repox.repox_cli import repox_app +from cocode.usage_stats import declare_cocode_usage_stats_host from cocode.validation_cli import validation_app @@ -65,6 +66,8 @@ def get_command(self, ctx: Context, cmd_name: str) -> Optional[Command]: @app.callback(invoke_without_command=True) def main(ctx: TyperContext) -> None: """Initialize Pipelex system before any command runs.""" + # Before the boot, which reads the declaration along with pipelex's other usage-statistics switches. + declare_cocode_usage_stats_host() Pipelex.make(library_dirs=PIPELINE_LIBRARY_DIRS) if ctx.invoked_subcommand is None: diff --git a/cocode/usage_stats.py b/cocode/usage_stats.py new file mode 100644 index 0000000..e4444a4 --- /dev/null +++ b/cocode/usage_stats.py @@ -0,0 +1,23 @@ +"""cocode's declaration to the anonymous usage statistics of the pipelex runtime it embeds. + +cocode sends no statistics of its own: the pipelex runtime reports the runs cocode drives through its own client, and +cocode only names itself as their `host`, so those runs can be told apart from the rest. The declaration is the +`PIPELEX_USAGE_STATS_HOST` variable, which pipelex reads with its other usage-statistics switches when it boots, and +which every process cocode starts inherits. pipelex's statistics client also takes the declaration through a Python +call, but the variable is the route pipelex documents, and it works whatever pipelex release cocode pins: a release +that sends no statistics simply ignores it. +""" + +import os + +PIPELEX_USAGE_STATS_HOST_VAR = "PIPELEX_USAGE_STATS_HOST" +COCODE_USAGE_STATS_HOST = "cocode" + + +def declare_cocode_usage_stats_host() -> None: + """Declare cocode as the program embedding pipelex, before cocode boots it. + + A host the environment already declares is kept: a program that starts cocode with its own declaration is the one + the runs belong to. + """ + os.environ.setdefault(PIPELEX_USAGE_STATS_HOST_VAR, COCODE_USAGE_STATS_HOST) diff --git a/docs/pages/usage-statistics.md b/docs/pages/usage-statistics.md new file mode 100644 index 0000000..023a79e --- /dev/null +++ b/docs/pages/usage-statistics.md @@ -0,0 +1,25 @@ +--- +title: Usage Statistics +--- + +# Usage Statistics + +cocode sends no usage statistics of its own. It runs its analyses on the Pipelex runtime, which collects **anonymous usage statistics** from the Pipelex release whose changelog announces them: counts of runs, pipe types, models and tokens. Pipelex never collects your methods, prompts, inputs, outputs, file names or pipe names, so nothing of the code cocode reads is sent. What is collected, where it goes and every way to turn it off is on Pipelex's [Usage Statistics](https://docs.pipelex.com/latest/setup/usage-statistics/) page. + +## cocode is named as the host + +Each event can name the program that embeds Pipelex in its `host` property, so the runs that program drives can be told apart from the rest. Before it boots Pipelex, cocode sets `PIPELEX_USAGE_STATS_HOST=cocode` in its own process, so the runs it drives carry `"host": "cocode"`. A value the environment already sets is kept. + +## Turning them off + +Every switch Pipelex offers turns them off for cocode too. For example: + +```bash +# For one cocode run +DO_NOT_TRACK=1 cocode changelog update v1.0.0 + +# For every Pipelex tool on this machine +pipelex usage-stats disable +``` + +The full list, which includes a project setting you can commit for everyone working in a repository, is in Pipelex's [Turning them off](https://docs.pipelex.com/latest/setup/usage-statistics/#turning-them-off) section. diff --git a/mkdocs.yml b/mkdocs.yml index d95ed27..bb64f13 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -71,6 +71,7 @@ nav: - Getting Started: pages/getting-started.md - Commands: pages/commands.md - Examples: pages/examples.md + - Usage Statistics: pages/usage-statistics.md - Contributing: contributing.md extra_css: diff --git a/tests/unit/test_usage_stats.py b/tests/unit/test_usage_stats.py new file mode 100644 index 0000000..26bb7c1 --- /dev/null +++ b/tests/unit/test_usage_stats.py @@ -0,0 +1,37 @@ +import os + +import pytest +from pytest_mock import MockerFixture +from typer import Context as TyperContext + +from cocode.cli.main import main +from cocode.usage_stats import COCODE_USAGE_STATS_HOST, PIPELEX_USAGE_STATS_HOST_VAR + + +class TestUsageStatsHost: + @pytest.mark.parametrize( + ("declared_host", "expected_host_at_boot"), + [ + (None, COCODE_USAGE_STATS_HOST), + ("mthds-agent", "mthds-agent"), + ], + ) + def test_host_is_declared_before_pipelex_boots(self, mocker: MockerFixture, declared_host: str | None, expected_host_at_boot: str) -> None: + """The CLI declares `cocode` as the host before it boots pipelex, and keeps a host the environment already declares.""" + mocker.patch.dict(os.environ) + os.environ.pop(PIPELEX_USAGE_STATS_HOST_VAR, None) + if declared_host is not None: + os.environ[PIPELEX_USAGE_STATS_HOST_VAR] = declared_host + + hosts_seen_at_boot: list[str | None] = [] + + def record_host_at_boot(**_kwargs: object) -> None: + hosts_seen_at_boot.append(os.environ.get(PIPELEX_USAGE_STATS_HOST_VAR)) + + mocker.patch("cocode.cli.main.Pipelex.make", side_effect=record_host_at_boot) + ctx = mocker.Mock(spec=TyperContext) + ctx.invoked_subcommand = "validate" + + main(ctx=ctx) + + assert hosts_seen_at_boot == [expected_host_at_boot] From affdd7678a1a17dee546285f47c3b8d0fece6ef0 Mon Sep 17 00:00:00 2001 From: Louis Choquel Date: Sun, 11 Oct 2026 22:48:04 +0200 Subject: [PATCH 2/3] Keep cocode's tests and CI from sending usage statistics The root test conftest forces DO_NOT_TRACK=1 when it loads, before anything boots pipelex, and the tests workflow sets DO_NOT_TRACK: "1" in its job env. DO_NOT_TRACK is the off switch every Pipelex tool honours and needs no pipelex API, so it works with the pinned pipelex and keeps working after the bump. A unit test loads the conftest again and checks it forces the switch, even over DO_NOT_TRACK=0. Co-Authored-By: Claude Opus 5.5 --- .github/workflows/tests-check.yml | 2 ++ tests/conftest.py | 6 ++++++ tests/unit/test_tests_never_report.py | 25 +++++++++++++++++++++++++ 3 files changed, 33 insertions(+) create mode 100644 tests/unit/test_tests_never_report.py diff --git a/.github/workflows/tests-check.yml b/.github/workflows/tests-check.yml index 7cf04b8..a4fe43b 100644 --- a/.github/workflows/tests-check.yml +++ b/.github/workflows/tests-check.yml @@ -21,6 +21,8 @@ jobs: env: VIRTUAL_ENV: ${{ github.workspace }}/.venv ENV: dev + # Our CI never sends usage statistics: the off switch every Pipelex tool honours. + DO_NOT_TRACK: "1" steps: - uses: actions/checkout@v4 diff --git a/tests/conftest.py b/tests/conftest.py index 40e88c3..b8bdf60 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,5 +1,6 @@ import asyncio import logging +import os from pathlib import Path import pytest @@ -20,6 +21,11 @@ from cocode.common import PIPELINE_LIBRARY_DIRS +# Our tests never report usage statistics, whatever pipelex they run against. `DO_NOT_TRACK` is the off switch every +# Pipelex tool honours and needs no pipelex API; it is forced, not defaulted, so a `DO_NOT_TRACK=0` in the shell cannot +# turn statistics back on. Set when this conftest loads, before any fixture or plugin hook boots pipelex. +os.environ["DO_NOT_TRACK"] = "1" + PIPELINE_LIBRARY_DIRS_FOR_TESTS = [*PIPELINE_LIBRARY_DIRS, Path("tests/pipelines")] pytest_plugins = [ diff --git a/tests/unit/test_tests_never_report.py b/tests/unit/test_tests_never_report.py new file mode 100644 index 0000000..10e4044 --- /dev/null +++ b/tests/unit/test_tests_never_report.py @@ -0,0 +1,25 @@ +import importlib.util +import os +from pathlib import Path + +import pytest +from pytest_mock import MockerFixture + +ROOT_CONFTEST_PATH = Path(__file__).parents[1] / "conftest.py" + + +class TestTestsNeverReport: + @pytest.mark.parametrize("do_not_track_before", [None, "0"]) + def test_root_conftest_forces_do_not_track(self, mocker: MockerFixture, do_not_track_before: str | None) -> None: + """Loading the root conftest sets `DO_NOT_TRACK=1`, whatever the environment held before, so no test sends usage statistics.""" + mocker.patch.dict(os.environ) + os.environ.pop("DO_NOT_TRACK", None) + if do_not_track_before is not None: + os.environ["DO_NOT_TRACK"] = do_not_track_before + + spec = importlib.util.spec_from_file_location("cocode_root_conftest_loaded_again", ROOT_CONFTEST_PATH) + assert spec is not None + assert spec.loader is not None + spec.loader.exec_module(importlib.util.module_from_spec(spec)) + + assert os.environ.get("DO_NOT_TRACK") == "1" From 1aafd5993d62ff6209ec3345e681c162f1069682 Mon Sep 17 00:00:00 2001 From: Louis Choquel Date: Sun, 11 Oct 2026 22:53:38 +0200 Subject: [PATCH 3/3] Turn test statistics off without tripping the pinned Gateway check pipelex 0.46.4 refuses to boot with inference when DO_NOT_TRACK is set and the Pipelex Gateway is enabled, as cocode's committed backends do, so the root conftest now forces PIPELEX_USAGE_STATS=0 instead: an off value for pipelex's statistics that touches nothing else. CI keeps DO_NOT_TRACK=1, with a comment saying why it is safe there: make gha-tests always passes --disable-inference. The usage-statistics docs page and changelog line now say cocode reports as host cocode from the Pipelex release that introduces the statistics, send nothing before, and drop the examples that fail on the pinned pipelex. Co-Authored-By: Claude Opus 5.5 --- .github/workflows/tests-check.yml | 4 +++- CHANGELOG.md | 2 +- docs/pages/usage-statistics.md | 20 +++----------------- tests/conftest.py | 10 ++++++---- tests/unit/test_tests_never_report.py | 14 +++++++------- 5 files changed, 20 insertions(+), 30 deletions(-) diff --git a/.github/workflows/tests-check.yml b/.github/workflows/tests-check.yml index a4fe43b..f2a6a6f 100644 --- a/.github/workflows/tests-check.yml +++ b/.github/workflows/tests-check.yml @@ -21,7 +21,9 @@ jobs: env: VIRTUAL_ENV: ${{ github.workspace }}/.venv ENV: dev - # Our CI never sends usage statistics: the off switch every Pipelex tool honours. + # Our CI never sends usage statistics: the off switch every Pipelex tool honours. Safe on the pinned pipelex, which + # refuses DO_NOT_TRACK only when it boots with inference and the Pipelex Gateway enabled, because `make gha-tests` + # always passes --disable-inference. DO_NOT_TRACK: "1" steps: - uses: actions/checkout@v4 diff --git a/CHANGELOG.md b/CHANGELOG.md index deeb297..1c63c90 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,7 +3,7 @@ ## [Unreleased] ### Added -- **Usage statistics:** cocode now names itself as the `host` of the anonymous usage statistics sent by the Pipelex runtime it embeds, by setting `PIPELEX_USAGE_STATS_HOST=cocode` before it boots Pipelex unless the environment already sets it. cocode sends no statistics of its own, and a Pipelex release that sends none ignores the variable. The new Usage Statistics page in the docs says what this means and how to turn the statistics off. +- **Usage statistics:** cocode sets `PIPELEX_USAGE_STATS_HOST=cocode` before it boots Pipelex, unless the environment already sets it, so that once cocode runs on the Pipelex release that introduces anonymous usage statistics, the runs it drives report as host `cocode`. cocode sends no statistics of its own, and a Pipelex release without them, like the one this version pins, sends none and ignores the variable. The new Usage Statistics page in the docs explains this and links to Pipelex's page for turning the statistics off. ### Fixed - **Docs:** `CLI_README.md`'s example of extracting Python files from the cookbook now reads its `recipes` directory. The cookbook's redesign removed the `examples` directory it pointed at, so the example extracted nothing. diff --git a/docs/pages/usage-statistics.md b/docs/pages/usage-statistics.md index 023a79e..d234022 100644 --- a/docs/pages/usage-statistics.md +++ b/docs/pages/usage-statistics.md @@ -4,22 +4,8 @@ title: Usage Statistics # Usage Statistics -cocode sends no usage statistics of its own. It runs its analyses on the Pipelex runtime, which collects **anonymous usage statistics** from the Pipelex release whose changelog announces them: counts of runs, pipe types, models and tokens. Pipelex never collects your methods, prompts, inputs, outputs, file names or pipe names, so nothing of the code cocode reads is sent. What is collected, where it goes and every way to turn it off is on Pipelex's [Usage Statistics](https://docs.pipelex.com/latest/setup/usage-statistics/) page. +cocode sends no usage statistics of its own. It runs its analyses on the Pipelex runtime, and Pipelex collects **anonymous usage statistics** from the Pipelex release that introduces them. Once cocode moves to that release, the runs it drives report through Pipelex's statistics and name cocode as their host, `"host": "cocode"`, so they can be told apart from the rest. Before that, nothing is sent. -## cocode is named as the host +cocode declares itself by setting `PIPELEX_USAGE_STATS_HOST=cocode` in its own process before it boots Pipelex, and keeps any value the environment already sets. -Each event can name the program that embeds Pipelex in its `host` property, so the runs that program drives can be told apart from the rest. Before it boots Pipelex, cocode sets `PIPELEX_USAGE_STATS_HOST=cocode` in its own process, so the runs it drives carry `"host": "cocode"`. A value the environment already sets is kept. - -## Turning them off - -Every switch Pipelex offers turns them off for cocode too. For example: - -```bash -# For one cocode run -DO_NOT_TRACK=1 cocode changelog update v1.0.0 - -# For every Pipelex tool on this machine -pipelex usage-stats disable -``` - -The full list, which includes a project setting you can commit for everyone working in a repository, is in Pipelex's [Turning them off](https://docs.pipelex.com/latest/setup/usage-statistics/#turning-them-off) section. +What Pipelex collects, what it never collects, where it goes and every way to turn it off are on Pipelex's [Usage Statistics](https://docs.pipelex.com/latest/setup/usage-statistics/) page. Each switch described there turns the statistics off for cocode too. diff --git a/tests/conftest.py b/tests/conftest.py index b8bdf60..2c97279 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -21,10 +21,12 @@ from cocode.common import PIPELINE_LIBRARY_DIRS -# Our tests never report usage statistics, whatever pipelex they run against. `DO_NOT_TRACK` is the off switch every -# Pipelex tool honours and needs no pipelex API; it is forced, not defaulted, so a `DO_NOT_TRACK=0` in the shell cannot -# turn statistics back on. Set when this conftest loads, before any fixture or plugin hook boots pipelex. -os.environ["DO_NOT_TRACK"] = "1" +# Our tests never report usage statistics, whatever pipelex they run against. `PIPELEX_USAGE_STATS=0` turns pipelex's +# statistics off and touches nothing else, so a pipelex that has no statistics ignores it. `DO_NOT_TRACK` would turn +# them off too, but the pinned pipelex refuses to boot with inference while it is set and the Pipelex Gateway is enabled. +# Forced, not defaulted, so no value in the shell turns statistics back on, and set when this conftest loads, before any +# fixture or plugin hook boots pipelex. +os.environ["PIPELEX_USAGE_STATS"] = "0" PIPELINE_LIBRARY_DIRS_FOR_TESTS = [*PIPELINE_LIBRARY_DIRS, Path("tests/pipelines")] diff --git a/tests/unit/test_tests_never_report.py b/tests/unit/test_tests_never_report.py index 10e4044..35bf90b 100644 --- a/tests/unit/test_tests_never_report.py +++ b/tests/unit/test_tests_never_report.py @@ -9,17 +9,17 @@ class TestTestsNeverReport: - @pytest.mark.parametrize("do_not_track_before", [None, "0"]) - def test_root_conftest_forces_do_not_track(self, mocker: MockerFixture, do_not_track_before: str | None) -> None: - """Loading the root conftest sets `DO_NOT_TRACK=1`, whatever the environment held before, so no test sends usage statistics.""" + @pytest.mark.parametrize("usage_stats_before", [None, "1"]) + def test_root_conftest_turns_usage_stats_off(self, mocker: MockerFixture, usage_stats_before: str | None) -> None: + """Loading the root conftest sets `PIPELEX_USAGE_STATS=0`, whatever the environment held before, so no test sends usage statistics.""" mocker.patch.dict(os.environ) - os.environ.pop("DO_NOT_TRACK", None) - if do_not_track_before is not None: - os.environ["DO_NOT_TRACK"] = do_not_track_before + os.environ.pop("PIPELEX_USAGE_STATS", None) + if usage_stats_before is not None: + os.environ["PIPELEX_USAGE_STATS"] = usage_stats_before spec = importlib.util.spec_from_file_location("cocode_root_conftest_loaded_again", ROOT_CONFTEST_PATH) assert spec is not None assert spec.loader is not None spec.loader.exec_module(importlib.util.module_from_spec(spec)) - assert os.environ.get("DO_NOT_TRACK") == "1" + assert os.environ.get("PIPELEX_USAGE_STATS") == "0"