From c6441f38845080aa5c36847ed102ceea5426de62 Mon Sep 17 00:00:00 2001 From: "Wei-Chun, Chang" Date: Wed, 16 Sep 2026 10:36:56 +0800 Subject: [PATCH] feat(telemetry): report the installed dbt version on the command event Recce OSS fails to start when dbt v2 is the only dbt in the environment, because dbt v2 drops the Python modules the dbt adapter imports. In the event data that failure looks the same as a machine with no dbt installed, so we cannot count the users it affects. The command event now carries the installed dbt version. The value comes from package metadata, not from an import, so it still arrives when the dbt import is what failed. Co-Authored-By: Claude Opus 5 (1M context) Signed-off-by: Wei-Chun, Chang --- recce/track.py | 18 ++++++++++ tests/test_track.py | 83 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 101 insertions(+) create mode 100644 tests/test_track.py diff --git a/recce/track.py b/recce/track.py index 6f2f4a780..7c5a35b84 100644 --- a/recce/track.py +++ b/recce/track.py @@ -19,6 +19,23 @@ _event_initialized = False +def _installed_dbt_version(): + """Installed dbt version from package metadata. + + Reads metadata, not an import: the import fails when dbt v2 is installed. + dbt v2 ships as `dbt`, dbt 1.x as `dbt-core`. Read `dbt-core` first, so a + mixed environment reports the one Recce uses. + """ + from importlib.metadata import PackageNotFoundError, version + + for dist in ("dbt-core", "dbt"): + try: + return version(dist) + except PackageNotFoundError: + continue + return None + + def _initialize_event_tracking() -> None: """Initialize event tracking at most once across concurrent commands.""" global _event_initialized @@ -143,6 +160,7 @@ def invoke(self, ctx: Context) -> t.Any: status=status, reason=reason, duration=duration, + dbt_version=_installed_dbt_version(), cloud=ctx.params.get("cloud", False), review=ctx.params.get("review", False), debug=ctx.params.get("debug", False), diff --git a/tests/test_track.py b/tests/test_track.py new file mode 100644 index 000000000..16a7d7281 --- /dev/null +++ b/tests/test_track.py @@ -0,0 +1,83 @@ +"""Tests for recce/track.py: the props TrackCommand puts in the `command` event.""" + +from importlib.metadata import PackageNotFoundError +from unittest.mock import patch + +import click +import pytest +from click.testing import CliRunner + +from recce.track import TrackCommand, _installed_dbt_version + + +def _fake_version(installed): + def lookup(dist): + if dist in installed: + return installed[dist] + raise PackageNotFoundError(dist) + + return lookup + + +class TestInstalledDbtVersion: + def test_reports_dbt_core_for_dbt_1x(self): + with patch("importlib.metadata.version", _fake_version({"dbt-core": "1.12.4"})): + assert _installed_dbt_version() == "1.12.4" + + def test_reports_dbt_distribution_for_dbt_v2(self): + with patch("importlib.metadata.version", _fake_version({"dbt": "2.0.2"})): + assert _installed_dbt_version() == "2.0.2" + + def test_prefers_dbt_core_when_both_present(self): + installed = {"dbt-core": "1.12.4", "dbt": "2.0.2"} + with patch("importlib.metadata.version", _fake_version(installed)): + assert _installed_dbt_version() == "1.12.4" + + def test_returns_none_when_no_dbt(self): + with patch("importlib.metadata.version", _fake_version({})): + assert _installed_dbt_version() is None + + +@pytest.fixture +def command_event(): + """Invoke a TrackCommand and return the props of the `command` event.""" + with ( + patch("recce.event.init"), + patch("recce.event.flush_events"), + patch("recce.event.flush_exceptions"), + patch("recce.event.capture_exception"), + patch("recce.event.set_exception_tag"), + patch("recce.event.log_codespaces_events"), + patch("recce.event.log_event") as mock_log, + ): + + def invoke(body): + @click.command(cls=TrackCommand) + def cmd(): + body() + + CliRunner().invoke(cmd, []) + props, event_type = mock_log.call_args[0] + assert event_type == "command" + return props + + yield invoke + + +class TestCommandEventProp: + def test_prop_sent_on_success(self, command_event): + with patch("importlib.metadata.version", _fake_version({"dbt-core": "1.12.4"})): + props = command_event(lambda: None) + assert props["dbt_version"] == "1.12.4" + + def test_prop_sent_when_dbt_import_fails(self, command_event): + """The dbt v2 case: the command fails on the missing dbt 1.x import, and + the version still has to reach the event.""" + + def body(): + raise ModuleNotFoundError("No module named 'agate'") + + with patch("importlib.metadata.version", _fake_version({"dbt": "2.0.2"})): + props = command_event(body) + assert props["dbt_version"] == "2.0.2" + assert props["status"] is False