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
28 changes: 28 additions & 0 deletions reflex/utils/telemetry.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,13 @@
import importlib.metadata
import json
import multiprocessing
import os
import platform
import sys
import warnings
from contextlib import suppress
from datetime import datetime, timezone
from pathlib import Path
from typing import Any, TypedDict, cast

from reflex_base import constants
Expand Down Expand Up @@ -166,6 +169,31 @@ def get_cpu_count() -> int:
return multiprocessing.cpu_count()


def is_in_virtualenv() -> bool:
"""Whether the current Python is running inside a virtual environment.

Returns:
True if a virtual environment appears to be active.
"""
if sys.prefix != sys.base_prefix:
return True
return bool(os.environ.get("VIRTUAL_ENV"))


def get_init_environment() -> dict[str, bool]:
"""Return Python tooling flags for the current working directory.

Returns:
A dict with ``in_virtualenv``, ``has_pyproject_toml`` and
``has_requirements_txt`` boolean flags.
"""
return {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

short circuit when telemetry is not enabled

"in_virtualenv": is_in_virtualenv(),
"has_pyproject_toml": Path(constants.PyprojectToml.FILE).exists(),
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

add has_uv_lock and has_reflex_lock (directory) here

"has_requirements_txt": Path(constants.RequirementsTxt.FILE).exists(),
}


def get_reflex_enterprise_version() -> str | None:
"""Get the version of reflex-enterprise if installed.

Expand Down
6 changes: 5 additions & 1 deletion reflex/utils/templates.py
Original file line number Diff line number Diff line change
Expand Up @@ -386,6 +386,10 @@ def initialize_app(app_name: str, template: str | None = None) -> str | None:
telemetry.send("reinit")
return None

# Captured before scaffolding so the snapshot reflects the user's CWD,
# not files the template will create.
init_environment = telemetry.get_init_environment()
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

move this call above the constants.Config.FILE.exists() check and also include this information in the reinit event


templates: dict[str, Template] = {}

# Don't fetch app templates if the user directly asked for DEFAULT.
Expand All @@ -412,7 +416,7 @@ def initialize_app(app_name: str, template: str | None = None) -> str | None:
app_name=app_name, template=template, templates=templates
)

telemetry.send("init", template=template)
telemetry.send("init", template=template, properties=init_environment)

return template

Expand Down
82 changes: 82 additions & 0 deletions tests/units/test_telemetry.py
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,88 @@ def test_prepare_event_does_not_mutate_cached_defaults(event_defaults):
assert "duration_ms" not in event_defaults["properties"]


@pytest.fixture
def venv_state(monkeypatch: pytest.MonkeyPatch):
"""Force a deterministic `is_in_virtualenv` reading.

Returns:
A callable that overrides `sys.prefix`, `sys.base_prefix`, and the
`VIRTUAL_ENV` env-var for the duration of the test.
"""

def configure(*, prefix: str, base_prefix: str, virtual_env: str | None) -> None:
monkeypatch.setattr(telemetry.sys, "prefix", prefix)
monkeypatch.setattr(telemetry.sys, "base_prefix", base_prefix)
if virtual_env is None:
monkeypatch.delenv("VIRTUAL_ENV", raising=False)
else:
monkeypatch.setenv("VIRTUAL_ENV", virtual_env)

return configure


def test_is_in_virtualenv_detects_pep_405_venv(venv_state):
venv_state(prefix="/tmp/venv", base_prefix="/usr", virtual_env=None)
assert telemetry.is_in_virtualenv() is True


def test_is_in_virtualenv_falls_back_to_virtual_env_var(venv_state):
venv_state(prefix="/usr", base_prefix="/usr", virtual_env="/tmp/venv")
assert telemetry.is_in_virtualenv() is True


def test_is_in_virtualenv_returns_false_for_system_python(venv_state):
venv_state(prefix="/usr", base_prefix="/usr", virtual_env=None)
assert telemetry.is_in_virtualenv() is False


@pytest.fixture
def init_environment_cwd(tmp_path, monkeypatch: pytest.MonkeyPatch):
"""Chdir into a clean tmp dir and let the caller stage dependency files.

Returns:
The temporary directory now serving as the working directory.
"""
monkeypatch.chdir(tmp_path)
return tmp_path


def test_get_init_environment_reports_dependency_files(
init_environment_cwd, venv_state
):
(init_environment_cwd / "pyproject.toml").write_text("")
venv_state(prefix="/tmp/venv", base_prefix="/usr", virtual_env=None)

assert telemetry.get_init_environment() == {
"in_virtualenv": True,
"has_pyproject_toml": True,
"has_requirements_txt": False,
}


Comment thread
FarhanAliRaza marked this conversation as resolved.
def test_get_init_environment_reports_requirements_txt(
init_environment_cwd, venv_state
):
(init_environment_cwd / "requirements.txt").write_text("")
venv_state(prefix="/usr", base_prefix="/usr", virtual_env="/tmp/venv")

assert telemetry.get_init_environment() == {
"in_virtualenv": True,
"has_pyproject_toml": False,
"has_requirements_txt": True,
}


def test_get_init_environment_empty_directory(init_environment_cwd, venv_state):
venv_state(prefix="/usr", base_prefix="/usr", virtual_env=None)

assert telemetry.get_init_environment() == {
"in_virtualenv": False,
"has_pyproject_toml": False,
"has_requirements_txt": False,
}


def test_prepare_event_properties_override_kwargs(event_defaults):
"""If both kwargs and properties supply the same key, properties wins."""
event = telemetry._prepare_event(
Expand Down
Loading