Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
from reflex_base.vars.sequence import ArrayVar, LiteralStringVar
from reflex_components_sonner.toast import toast

from reflex.utils.telemetry_context import increment_feature
from reflex_components_core.base.fragment import Fragment
from reflex_components_core.core._upload import UploadChunkIterator, UploadFile
from reflex_components_core.core.cond import cond
Expand Down Expand Up @@ -288,6 +289,7 @@ def create(cls, *children, **props) -> Component:
"""
# Mark the Upload component as used in the app.
cls.is_used = True
increment_feature("upload_count")

props.setdefault("multiple", True)

Expand Down
2 changes: 1 addition & 1 deletion pyi_hashes.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
"packages/reflex-components-core/src/reflex_components_core/core/helmet.pyi": "7fd81a99bde5b0ff94bb52523597fd5c",
"packages/reflex-components-core/src/reflex_components_core/core/html.pyi": "753d6ae315369530dad450ed643f5be6",
"packages/reflex-components-core/src/reflex_components_core/core/sticky.pyi": "ba60a7d9cba75b27a1133bd63a9fbd59",
"packages/reflex-components-core/src/reflex_components_core/core/upload.pyi": "2dd6ba6e3a4d61fc1d79eb582a7cc548",
"packages/reflex-components-core/src/reflex_components_core/core/upload.pyi": "91d66d21b80fad4c0ebaa9c88274d2e2",
"packages/reflex-components-core/src/reflex_components_core/core/window_events.pyi": "5e1dcb1130bc8af282783fae329ae6a6",
"packages/reflex-components-core/src/reflex_components_core/datadisplay/__init__.pyi": "c96fed4da42a13576d64f84e3c7cb25c",
"packages/reflex-components-core/src/reflex_components_core/el/__init__.pyi": "f09129ddefb57ab4c7769c86dc9a3153",
Expand Down
11 changes: 9 additions & 2 deletions reflex/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,11 @@
should_prerender_routes,
)
from reflex.utils.misc import run_in_thread
from reflex.utils.telemetry_context import CompileTrigger, TelemetryContext
from reflex.utils.telemetry_context import (
CompileTrigger,
TelemetryContext,
increment_feature,
)
from reflex.utils.token_manager import RedisTokenManager, TokenManager

if sys.version_info < (3, 13):
Expand Down Expand Up @@ -907,7 +911,10 @@ def add_page(
# Setup dynamic args for the route.
# this state assignment is only required for tests using the deprecated state kwarg for App
state = self._state or State
state.setup_dynamic_args(get_route_args(route))
route_args = get_route_args(route)
state.setup_dynamic_args(route_args)
if route_args:
increment_feature("dynamic_routes_count")
Comment thread
FarhanAliRaza marked this conversation as resolved.
Outdated

self._load_events[route] = (
(on_load if isinstance(on_load, list) else [on_load])
Expand Down
5 changes: 5 additions & 0 deletions reflex/app_mixins/lifespan.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@
from reflex_base.utils.exceptions import InvalidLifespanTaskTypeError
from starlette.applications import Starlette

from reflex.utils.telemetry_context import increment_feature

from .mixin import AppMixin

if TYPE_CHECKING:
Expand Down Expand Up @@ -198,4 +200,7 @@ def register_lifespan_task(
functools.update_wrapper(registered_task, task)
self._lifespan_tasks[registered_task] = None
console.debug(f"Registered lifespan task: {task_name}")
module = getattr(registered_task, "__module__", None) or ""
if module != "reflex" and not module.startswith("reflex."):
increment_feature("lifespan_tasks_count")
return task
2 changes: 2 additions & 0 deletions reflex/istate/shared.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@

from reflex.istate.manager.token import BaseStateToken
from reflex.state import BaseState, State, _override_base_method
from reflex.utils.telemetry_context import increment_feature

UPDATE_OTHER_CLIENT_TASKS: set[asyncio.Task] = set()
LINKED_STATE = TypeVar("LINKED_STATE", bound="SharedStateBaseInternal")
Expand Down Expand Up @@ -519,3 +520,4 @@ def __init_subclass__(cls, **kwargs):
# pulls in all linked states and substates which may not actually be
# accessed for this event.
root_state._always_dirty_substates.add(SharedStateBaseInternal.get_name())
increment_feature("shared_state_count")
5 changes: 5 additions & 0 deletions reflex/istate/storage.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@

from reflex_base.utils import format

from reflex.utils.telemetry_context import increment_feature


class ClientStorageBase:
"""Base class for client-side storage."""
Expand Down Expand Up @@ -73,6 +75,7 @@ def __new__(
inst.domain = domain
inst.secure = secure
inst.same_site = same_site
increment_feature("cookie_count")
return inst


Expand Down Expand Up @@ -109,6 +112,7 @@ def __new__(
inst = super().__new__(cls, object)
inst.name = name
inst.sync = sync
increment_feature("local_storage_count")
return inst


Expand Down Expand Up @@ -141,4 +145,5 @@ def __new__(
else:
inst = super().__new__(cls, object)
inst.name = name
increment_feature("session_storage_count")
return inst
3 changes: 3 additions & 0 deletions reflex/model.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@
from reflex_base.utils import console
from reflex_base.utils.serializers import serializer

from reflex.utils.telemetry_context import increment_feature

if TYPE_CHECKING:
from typing import TypeVar

Expand Down Expand Up @@ -200,6 +202,7 @@ def register(cls, model: SQLModelOrSqlAlchemyT) -> SQLModelOrSqlAlchemyT:
The model passed in as an argument (Allows decorator usage)
"""
cls.models.add(model)
increment_feature("db_model_count")
return model

@classmethod
Expand Down
92 changes: 75 additions & 17 deletions reflex/utils/telemetry_accounting.py
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

[P2] reflex/utils/telemetry_accounting.py (line 270) double-counts inherited client-storage vars. _walk_state_features() walks state_cls.get_fields() for every state returned by _walk_states(), but child states include parent fields there. A root state with c: str = Cookie() and child state with ls: str = LocalStorage() reports cookie_count == 2 and local_storage_count == 1; the cookie is counted once on the root and again through the child’s inherited field. This affects cookie_count, local_storage_count, and session_storage_count for normal state hierarchies. It should probably walk non-inherited fields only, e.g. names from state_cls.base_vars, then resolve those names back to fields.

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

Original file line number Diff line number Diff line change
Expand Up @@ -3,19 +3,27 @@
from __future__ import annotations

from collections.abc import Iterable, Iterator
from typing import TYPE_CHECKING, Any, TypedDict
from typing import TYPE_CHECKING, TypedDict

from reflex_base.config import get_config
from reflex_base.utils import console

from reflex.utils import telemetry
from reflex.utils.telemetry_context import (
_KNOWN_FEATURES,
TelemetryContext,
_recorded_features,
increment_feature,
)

__all__ = ["increment_feature", "record_compile"]

if TYPE_CHECKING:
from reflex_base.components.component import BaseComponent

from reflex.app import App
from reflex.state import BaseState
from reflex.utils.telemetry_context import CompileTrigger, TelemetryContext
from reflex.utils.telemetry_context import CompileTrigger, FeatureName


class _StateStats(TypedDict):
Expand All @@ -42,7 +50,7 @@ class _CompileEventProperties(TypedDict):
pages_count: int
component_counts: dict[str, int]
states: list[_StateStats]
features_used: dict[str, Any]
features_used: dict[FeatureName, int]
duration_ms: int
trigger: CompileTrigger | None
exception: _ExceptionInfo | None
Expand Down Expand Up @@ -78,13 +86,14 @@ def _collect_compile_event_payload(
The properties dict to send to PostHog.
"""
config = get_config()
user_states = list(_walk_states(app._state))
return {
"plugins_enabled": [p.__class__.__name__ for p in config.plugins],
"plugins_disabled": [p.__name__ for p in config.disable_plugins],
"pages_count": len(app._pages),
"component_counts": _count_components(app._pages.values()),
"states": _collect_all_state_stats(app),
"features_used": dict(ctx.features_used),
"states": [_collect_state_stats(s) for s in user_states],
"features_used": _collect_features_used(ctx, user_states),
"duration_ms": ctx.elapsed_ms(),
"trigger": ctx.trigger,
"exception": _sanitize_exception(ctx.exception),
Expand Down Expand Up @@ -141,18 +150,6 @@ def _walk_states(root: type[BaseState] | None) -> Iterator[type[BaseState]]:
yield from _walk_states(sub)


def _collect_all_state_stats(app: App) -> list[_StateStats]:
"""Collect per-state statistics for every state attached to the app.

Args:
app: The compiled application.

Returns:
A list of per-state stat dicts.
"""
return [_collect_state_stats(state_cls) for state_cls in _walk_states(app._state)]


def _collect_state_stats(state_cls: type[BaseState]) -> _StateStats:
"""Collect structural statistics for a single state class.

Expand Down Expand Up @@ -191,3 +188,64 @@ def _sanitize_exception(exc: BaseException | None) -> _ExceptionInfo | None:
if exc is None:
return None
return {"type": type(exc).__name__}


def _collect_features_used(
ctx: TelemetryContext, user_states: list[type[BaseState]]
) -> dict[FeatureName, int]:
"""Build the ``features_used`` snapshot for the compile event.

Every known feature ships with its invocation count (zero by default), so
consumers don't have to distinguish "feature not used" from "detector broken."

Args:
ctx: The active telemetry context.
user_states: Pre-walked user state classes (shared with state stats).

Returns:
Dict of feature key -> invocation count.
"""
features: dict[FeatureName, int] = dict.fromkeys(_KNOWN_FEATURES, 0)
features.update(_recorded_features)
_record_config_attestations(features)
_record_background_handler_count(features, user_states)
features.update(ctx.features_used)
Comment thread
FarhanAliRaza marked this conversation as resolved.
Outdated
Comment thread
FarhanAliRaza marked this conversation as resolved.
Outdated
return features


_STATE_MANAGER_FEATURE: dict[str, FeatureName] = {
"disk": "state_manager_disk",
"memory": "state_manager_memory",
"redis": "state_manager_redis",
}


def _record_config_attestations(features: dict[FeatureName, int]) -> None:
"""Write config-derived feature counts (state-manager mode, CORS).

Args:
features: The snapshot to populate.
"""
config = get_config()
key = _STATE_MANAGER_FEATURE.get(config.state_manager_mode.value)
if key is not None:
features[key] = 1
if tuple(config.cors_allowed_origins) != ("*",):
features["cors_customized"] = 1


def _record_background_handler_count(
features: dict[FeatureName, int], user_states: list[type[BaseState]]
) -> None:
"""Count ``@rx.event(background=True)`` handlers across user states.

Args:
features: The snapshot to populate.
user_states: Pre-walked user state classes.
"""
features["background_event_handlers_count"] = sum(
1
for state_cls in user_states
for handler in state_cls.event_handlers.values()
if handler.is_background
)
42 changes: 40 additions & 2 deletions reflex/utils/telemetry_context.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

import dataclasses
import time
from typing import Any, Literal
from typing import Literal, get_args

from reflex_base.config import get_config
from reflex_base.context.base import BaseContext
Expand All @@ -13,13 +13,51 @@
"initial", "cli_compile", "backend_startup", "hot_reload", "export"
]

FeatureName = Literal[
"background_event_handlers_count",
"cookie_count",
"cors_customized",
"db_model_count",
"dynamic_routes_count",
"lifespan_tasks_count",
"local_storage_count",
"session_storage_count",
"shared_state_count",
"state_manager_disk",
"state_manager_memory",
"state_manager_redis",
"upload_count",
]

_KNOWN_FEATURES: tuple[FeatureName, ...] = get_args(FeatureName)

# Counters bumped outside an active compile context (import-time class
# definitions, decorators, registrations) accumulate here so they survive
# into the next compile event.
_recorded_features: dict[FeatureName, int] = {}


def increment_feature(name: FeatureName, by: int = 1) -> None:
"""Bump a feature invocation counter.

Writes to the active TelemetryContext if one is attached, else to the
process-level counter so import-time signals survive into the next compile.

Args:
name: The feature counter to bump.
by: How much to add. Defaults to 1.
"""
target = TelemetryContext.get()
target_dict = target.features_used if target is not None else _recorded_features
target_dict[name] = target_dict.get(name, 0) + by


@dataclasses.dataclass(frozen=True, kw_only=True, slots=True, eq=False)
class TelemetryContext(BaseContext):
"""Per-compile telemetry handle attached to the current contextvar."""

start_perf_counter: float = dataclasses.field(default_factory=time.perf_counter)
features_used: dict[str, Any] = dataclasses.field(default_factory=dict)
features_used: dict[FeatureName, int] = dataclasses.field(default_factory=dict)
trigger: CompileTrigger | None = None
exception: BaseException | None = dataclasses.field(default=None, repr=False)

Expand Down
Loading
Loading