analytics: send remote types and git-remote's path hashes (#10816)

This commit is contained in:
skshetry
2025-07-28 15:07:41 +05:45
committed by GitHub
parent ba1281a41c
commit dc13186759
4 changed files with 135 additions and 25 deletions
+71 -12
View File
@@ -1,10 +1,14 @@
import json import json
import os import os
from typing import TYPE_CHECKING, Optional
from dvc.log import logger from dvc.log import logger
from .env import DVC_ANALYTICS_ENDPOINT, DVC_NO_ANALYTICS from .env import DVC_ANALYTICS_ENDPOINT, DVC_NO_ANALYTICS
if TYPE_CHECKING:
from dvc.scm import Base
logger = logger.getChild(__name__) logger = logger.getChild(__name__)
@@ -89,20 +93,62 @@ def send(path):
os.remove(path) os.remove(path)
def _scm_in_use(): def _git_remote_url(scm: Optional["Base"]) -> Optional[str]:
from dvc.exceptions import NotDvcRepoError from dvc.scm import Git
from dvc.repo import Repo
from dvc.scm import NoSCM
from .scm import SCM, SCMError if not isinstance(scm, Git):
return None
from dulwich.porcelain import get_remote_repo
dulwich_repo = scm.dulwich.repo
try:
_remote, url = get_remote_repo(dulwich_repo)
except IndexError:
# IndexError happens when the head is detached
_remote, url = get_remote_repo(dulwich_repo, b"origin")
# Dulwich returns (None, "origin") if no remote set
if (_remote, url) == (None, "origin"):
return None
return url
def _scm_in_use(scm: Optional["Base"]) -> Optional[str]:
return type(scm).__name__ if scm else None
def _parse_git_remote_path(remote_url: str) -> str:
from urllib.parse import urlparse
from scmrepo.urls import is_scp_style_url
parsed = urlparse(remote_url)
# Windows Path also gets parsed with a drive letter as scheme
# https://github.com/python/cpython/issues/86381
if parsed.scheme and parsed.scheme in ("http", "https", "git", "ssh"):
return parsed.path.strip("/")
if is_scp_style_url(remote_url):
# handle scp-style URL
parts = remote_url.split(":", 1)
if len(parts) == 2:
_, path = parts
return path.rstrip("/")
return remote_url
def _git_remote_path_hash(scm: Optional["Base"]) -> Optional[str]:
"""Return a hash of the git remote path."""
import hashlib
try: try:
scm = SCM(root_dir=Repo.find_root()) if remote_url := _git_remote_url(scm):
return type(scm).__name__ path = _parse_git_remote_path(remote_url)
except SCMError: h = hashlib.md5(path.encode("utf-8"), usedforsecurity=False) # for FIPS
return NoSCM.__name__ return h.hexdigest()
except NotDvcRepoError: except Exception: # noqa: BLE001
pass logger.debug("Failed to get git remote path", exc_info=True)
return None
def _runtime_info(): def _runtime_info():
@@ -112,6 +158,8 @@ def _runtime_info():
from iterative_telemetry import _generate_ci_id, find_or_create_user_id from iterative_telemetry import _generate_ci_id, find_or_create_user_id
from dvc import __version__ from dvc import __version__
from dvc.info import _get_remotes
from dvc.repo import Repo
from dvc.utils import is_binary from dvc.utils import is_binary
ci_id = _generate_ci_id() ci_id = _generate_ci_id()
@@ -120,13 +168,24 @@ def _runtime_info():
else: else:
group_id, user_id = None, find_or_create_user_id() group_id, user_id = None, find_or_create_user_id()
scm = None
remotes = None
try:
repo = Repo()
scm = repo.scm
remotes = _get_remotes(repo.config)
except Exception as exc: # noqa: BLE001
logger.debug("failed to open repo: %s", exc)
return { return {
"dvc_version": __version__, "dvc_version": __version__,
"is_binary": is_binary(), "is_binary": is_binary(),
"scm_class": _scm_in_use(), "scm_class": _scm_in_use(scm),
"system_info": _system_info(), "system_info": _system_info(),
"user_id": user_id, "user_id": user_id,
"group_id": group_id, "group_id": group_id,
"remotes": remotes,
"git_remote_hash": _git_remote_path_hash(scm),
} }
+6 -5
View File
@@ -48,7 +48,10 @@ def get_dvc_info():
info.append("Cache types: " + error_link("no-dvc-cache")) info.append("Cache types: " + error_link("no-dvc-cache"))
info.append(f"Caches: {_get_caches(repo.cache)}") info.append(f"Caches: {_get_caches(repo.cache)}")
info.append(f"Remotes: {_get_remotes(repo.config)}")
configured_remotes = _get_remotes(repo.config)
remotes = ", ".join(configured_remotes) if configured_remotes else None
info.append(f"Remotes: {remotes}")
root_directory = repo.root_dir root_directory = repo.root_dir
fs_root = _get_fs_type(os.path.abspath(root_directory)) fs_root = _get_fs_type(os.path.abspath(root_directory))
@@ -75,12 +78,10 @@ def _get_caches(cache):
def _get_remotes(config): def _get_remotes(config):
schemes = ( return [
get_fs_cls(get_fs_config(config, name=remote)).protocol get_fs_cls(get_fs_config(config, name=remote)).protocol
for remote in config["remote"] for remote in config["remote"]
) ]
return ", ".join(schemes) or "None"
def _get_linktype_support_info(repo): def _get_linktype_support_info(repo):
+17 -6
View File
@@ -2,6 +2,7 @@ import os
import pytest import pytest
from dvc import __version__, env
from dvc.analytics import _scm_in_use, collect_and_send_report from dvc.analytics import _scm_in_use, collect_and_send_report
from dvc.cli import main from dvc.cli import main
from dvc.repo import Repo from dvc.repo import Repo
@@ -33,7 +34,8 @@ def mock_daemon(mocker):
return mocker.patch("dvc.daemon.daemon", mocker.MagicMock(side_effect=func)) return mocker.patch("dvc.daemon.daemon", mocker.MagicMock(side_effect=func))
def test_collect_and_send_report(mocker, dvc, mock_daemon): def test_collect_and_send_report(monkeypatch, mocker, dvc, mock_daemon):
monkeypatch.delenv(env.DVC_ANALYTICS_ENDPOINT, raising=False)
mock_post = mocker.patch("requests.post") mock_post = mocker.patch("requests.post")
collect_and_send_report() collect_and_send_report()
@@ -41,19 +43,28 @@ def test_collect_and_send_report(mocker, dvc, mock_daemon):
assert mock_post.call_count == 1 assert mock_post.call_count == 1
assert mock_post.call_args == mocker.call( assert mock_post.call_args == mocker.call(
"https://analytics.dvc.org", "https://analytics.dvc.org",
json=ANY(dict), json={
"dvc_version": __version__,
"scm_class": type(dvc.scm).__name__,
"is_binary": False,
"system_info": ANY(dict),
"user_id": ANY(str),
"group_id": mocker.ANY,
"remotes": ANY(list),
"git_remote_hash": None,
},
headers={"content-type": "application/json"}, headers={"content-type": "application/json"},
timeout=5, timeout=5,
) )
def test_scm_dvc_only(tmp_dir, dvc): def test_scm_dvc_only(tmp_dir, dvc):
scm = _scm_in_use() scm = _scm_in_use(dvc.scm)
assert scm == "NoSCM" assert scm == "NoSCM"
def test_scm_git(tmp_dir, scm, dvc): def test_scm_git(tmp_dir, scm, dvc):
scm = _scm_in_use() scm = _scm_in_use(scm)
assert scm == "Git" assert scm == "Git"
@@ -62,7 +73,7 @@ def test_scm_subrepo(tmp_dir, scm):
subdir.mkdir() subdir.mkdir()
with subdir.chdir(): with subdir.chdir():
Repo.init(subdir=True) repo = Repo.init(subdir=True)
scm = _scm_in_use() scm = _scm_in_use(repo.scm)
assert scm == "Git" assert scm == "Git"
+41 -2
View File
@@ -1,10 +1,11 @@
import hashlib
import json import json
import platform import platform
import pytest import pytest
from voluptuous import Any, Schema from voluptuous import Any, Schema
from dvc import analytics from dvc import analytics, env
from dvc.cli import parse_args from dvc.cli import parse_args
@@ -50,6 +51,8 @@ def test_runtime_info(tmp_global_dir):
"user_id": str, "user_id": str,
"system_info": dict, "system_info": dict,
"group_id": Any(str, None), "group_id": Any(str, None),
"remotes": Any(list, None),
"git_remote_hash": Any(str, None),
}, },
required=True, required=True,
) )
@@ -57,7 +60,8 @@ def test_runtime_info(tmp_global_dir):
assert schema(analytics._runtime_info()) assert schema(analytics._runtime_info())
def test_send(mocker, tmp_path): def test_send(monkeypatch, mocker, tmp_path):
monkeypatch.delenv(env.DVC_ANALYTICS_ENDPOINT, raising=False)
mock_post = mocker.patch("requests.post") mock_post = mocker.patch("requests.post")
import requests import requests
@@ -158,3 +162,38 @@ def test_system_info():
) )
assert schema(analytics._system_info()) assert schema(analytics._system_info())
@pytest.mark.parametrize(
"git_remote",
[
"git://github.com/iterative/dvc.git",
"[email protected]:iterative/dvc.git",
"http://github.com/iterative/dvc.git",
"https://github.com/iterative/dvc.git",
"ssh://[email protected]/iterative/dvc.git",
],
)
def test_git_remote_hash(mocker, git_remote):
m = mocker.patch("dvc.analytics._git_remote_url", return_value=git_remote)
expected = hashlib.md5(b"iterative/dvc.git").hexdigest()
assert analytics._git_remote_path_hash(None) == expected
m.assert_called_once_with(None)
@pytest.mark.parametrize(
"git_remote",
[
"C:\\Users\\user\\dvc.git",
"/home/user/dvc.git",
"file:///home/user/dvc.git",
"./dvc.git",
],
)
def test_git_remote_hash_local(mocker, git_remote):
m = mocker.patch("dvc.analytics._git_remote_url", return_value=git_remote)
expected = hashlib.md5(git_remote.encode("utf-8")).hexdigest()
assert analytics._git_remote_path_hash(None) == expected
m.assert_called_once_with(None)