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
2 changes: 2 additions & 0 deletions packages/reflex-base/src/reflex_base/constants/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@
GitIgnore,
PyprojectToml,
RequirementsTxt,
UvLock,
)
from .custom_components import CustomComponents
from .event import Endpoint, EventTriggers, SocketEvent
Expand Down Expand Up @@ -120,4 +121,5 @@
"SocketEvent",
"StateManagerMode",
"Templates",
"UvLock",
]
7 changes: 7 additions & 0 deletions packages/reflex-base/src/reflex_base/constants/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,13 @@ class RequirementsTxt(SimpleNamespace):
DEFAULTS_STUB = f"{Reflex.MODULE_NAME}=="


class UvLock(SimpleNamespace):
"""uv.lock constants."""

# The uv lockfile.
FILE = "uv.lock"


class DefaultPorts(SimpleNamespace):
"""Default port constants."""

Expand Down
2 changes: 2 additions & 0 deletions reflex/constants/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@
GitIgnore,
PyprojectToml,
RequirementsTxt,
UvLock,
)
from .custom_components import CustomComponents
from .event import Endpoint, EventTriggers, SocketEvent
Expand Down Expand Up @@ -115,4 +116,5 @@
"SocketEvent",
"StateManagerMode",
"Templates",
"UvLock",
]
38 changes: 36 additions & 2 deletions reflex/utils/telemetry.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,17 @@
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
from reflex_base.config import get_config
from reflex_base.environment import environment
from reflex_base.utils.decorator import once, once_unless_none
from reflex_base.utils.exceptions import ReflexError
Expand Down Expand Up @@ -166,6 +170,38 @@ 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``,
``has_requirements_txt``, ``has_uv_lock`` and ``has_reflex_lock``
boolean flags, or an empty dict when telemetry is disabled (so the
filesystem stats are skipped when their results would be discarded).
"""
if not get_config().telemetry_enabled:
return {}

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

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

fixed

"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

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

added

"has_requirements_txt": Path(constants.RequirementsTxt.FILE).exists(),
"has_uv_lock": Path(constants.UvLock.FILE).exists(),
"has_reflex_lock": Path(constants.Bun.ROOT_LOCKFILE_DIR).is_dir(),
}


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

Expand Down Expand Up @@ -349,8 +385,6 @@ def _send(
properties: dict[str, Any] | None = None,
**kwargs,
) -> bool:
from reflex_base.config import get_config

# Get the telemetry_enabled from the config if it is not specified.
if telemetry_enabled is None:
telemetry_enabled = get_config().telemetry_enabled
Expand Down
7 changes: 5 additions & 2 deletions reflex/utils/templates.py
Original file line number Diff line number Diff line change
Expand Up @@ -381,9 +381,12 @@ def initialize_app(app_name: str, template: str | None = None) -> str | None:
# Local imports to avoid circular imports.
from reflex.utils import telemetry

# Snapshot must reflect the user's CWD, not files the template would create.
init_environment = telemetry.get_init_environment()

# Check if the app is already initialized.
if constants.Config.FILE.exists():
telemetry.send("reinit")
telemetry.send("reinit", properties=init_environment)
return None

templates: dict[str, Template] = {}
Expand Down Expand Up @@ -412,7 +415,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
127 changes: 127 additions & 0 deletions tests/units/test_telemetry.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
from types import SimpleNamespace

import pytest
from packaging.version import parse as parse_python_version
from pytest_mock import MockerFixture
Expand Down Expand Up @@ -198,6 +200,131 @@ 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 patch_telemetry_config(mocker: MockerFixture):
"""Patch ``telemetry.get_config`` with a stub of a chosen ``telemetry_enabled``.

Returns:
A callable ``patch(enabled)`` that installs the mock on demand.
"""

def patch(*, enabled: bool) -> None:
mocker.patch(
"reflex.utils.telemetry.get_config",
return_value=SimpleNamespace(telemetry_enabled=enabled),
)

return patch


@pytest.fixture
def init_environment_cwd(
tmp_path, monkeypatch: pytest.MonkeyPatch, patch_telemetry_config
):
"""Chdir into a clean tmp dir and force telemetry-enabled config.

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


def test_get_init_environment_reports_dependency_files(
init_environment_cwd, venv_state
):
(init_environment_cwd / "pyproject.toml").write_text("")
(init_environment_cwd / "uv.lock").write_text("")
(init_environment_cwd / "reflex.lock").mkdir()
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,
"has_uv_lock": True,
"has_reflex_lock": True,
}


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,
"has_uv_lock": False,
"has_reflex_lock": False,
}


Comment thread
FarhanAliRaza marked this conversation as resolved.
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,
"has_uv_lock": False,
"has_reflex_lock": False,
}


def test_get_init_environment_short_circuits_when_telemetry_disabled(
tmp_path, monkeypatch: pytest.MonkeyPatch, patch_telemetry_config
):
"""When telemetry is disabled the env snapshot is skipped entirely.

A pyproject.toml is staged so a non-short-circuiting implementation would
surface ``has_pyproject_toml: True`` instead of an empty dict.
"""
monkeypatch.chdir(tmp_path)
(tmp_path / "pyproject.toml").write_text("")
patch_telemetry_config(enabled=False)

assert telemetry.get_init_environment() == {}


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