Skip to content

Commit a6b877d

Browse files
committed
refactor(telemetry): thread Config and route helpers through feature collector
Drop the intermediate ``_ComponentWalk`` TypedDict in favor of a plain tuple, pass the resolved ``Config`` and ``get_route_args`` into the feature collector instead of re-resolving them, and lean on the ``_KNOWN_FEATURES`` zero-fill to drop the redundant storage-counter seeding pass.
1 parent d0bd58f commit a6b877d

2 files changed

Lines changed: 89 additions & 128 deletions

File tree

reflex/utils/telemetry_accounting.py

Lines changed: 38 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,9 @@
44

55
from collections.abc import Iterable, Iterator
66
from importlib.util import find_spec
7-
from typing import TYPE_CHECKING, Any, TypedDict
7+
from typing import TYPE_CHECKING, TypedDict
88

9-
from reflex_base.config import get_config
9+
from reflex_base.config import Config, get_config
1010
from reflex_base.telemetry_context import _KNOWN_FEATURES, TelemetryContext
1111
from reflex_base.utils import console
1212
from reflex_components_core.core.upload import Upload
@@ -19,6 +19,7 @@
1919
SessionStorage,
2020
)
2121
from reflex.model import ModelRegistry
22+
from reflex.route import get_route_args
2223
from reflex.utils import telemetry
2324

2425
_HAS_SQLALCHEMY = find_spec("sqlalchemy") is not None
@@ -28,6 +29,7 @@
2829
if TYPE_CHECKING:
2930
from reflex_base.components.component import BaseComponent
3031
from reflex_base.telemetry_context import CompileTrigger, FeatureName
32+
from reflex_base.vars import Field
3133

3234
from reflex.app import App
3335
from reflex.state import BaseState
@@ -63,13 +65,6 @@ class _CompileEventProperties(TypedDict):
6365
exception: _ExceptionInfo | None
6466

6567

66-
class _ComponentWalk(TypedDict):
67-
"""Aggregated outputs from a single pass over the compiled page trees."""
68-
69-
counts: dict[str, int]
70-
upload_count: int
71-
72-
7368
def record_compile(app: App, ctx: TelemetryContext) -> None:
7469
"""Build the compile-event payload and send it to PostHog.
7570
@@ -101,22 +96,24 @@ def _collect_compile_event_payload(
10196
"""
10297
config = get_config()
10398
user_states = list(_walk_states(app._state))
104-
component_walk = _walk_components(app._pages.values())
99+
component_counts, upload_count = _walk_components(app._pages.values())
105100
return {
106101
"plugins_enabled": [p.__class__.__name__ for p in config.plugins],
107102
"plugins_disabled": [p.__name__ for p in config.disable_plugins],
108103
"pages_count": len(app._pages),
109-
"component_counts": component_walk["counts"],
104+
"component_counts": component_counts,
110105
"states": [_collect_state_stats(s) for s in user_states],
111-
"features_used": _collect_features_used(app, user_states, component_walk),
106+
"features_used": _collect_features_used(app, config, user_states, upload_count),
112107
"duration_ms": ctx.elapsed_ms(),
113108
"trigger": ctx.trigger,
114109
"exception": _sanitize_exception(ctx.exception),
115110
}
116111

117112

118-
def _walk_components(pages: Iterable[BaseComponent]) -> _ComponentWalk:
119-
"""Walk page trees once and aggregate every telemetry signal we need.
113+
def _walk_components(
114+
pages: Iterable[BaseComponent],
115+
) -> tuple[dict[str, int], int]:
116+
"""Walk page trees once and aggregate class-name counts and the upload total.
120117
121118
Auto-memoized components live in the tree as dynamic
122119
``ExperimentalMemoComponent_<Type>_<tag>_<hash>`` subclasses. Bucketing by
@@ -128,8 +125,9 @@ def _walk_components(pages: Iterable[BaseComponent]) -> _ComponentWalk:
128125
pages: Component-tree roots to walk.
129126
130127
Returns:
131-
A dict with ``counts`` (class name to occurrence count) and
132-
``upload_count`` (instances of ``Upload`` or its subclasses).
128+
``(counts, upload_count)`` where ``counts`` maps class name to
129+
occurrence count and ``upload_count`` is the number of ``Upload``
130+
instances (including subclasses).
133131
"""
134132
counts: dict[str, int] = {}
135133
upload_count = 0
@@ -144,7 +142,7 @@ def _walk_components(pages: Iterable[BaseComponent]) -> _ComponentWalk:
144142
counts[name] = counts.get(name, 0) + 1
145143
if node.children:
146144
stack.extend(node.children)
147-
return {"counts": counts, "upload_count": upload_count}
145+
return counts, upload_count
148146

149147

150148
def _walk_states(root: type[BaseState] | None) -> Iterator[type[BaseState]]:
@@ -211,31 +209,32 @@ def _sanitize_exception(exc: BaseException | None) -> _ExceptionInfo | None:
211209

212210
def _collect_features_used(
213211
app: App,
212+
config: Config,
214213
user_states: list[type[BaseState]],
215-
component_walk: _ComponentWalk,
214+
upload_count: int,
216215
) -> dict[FeatureName, int]:
217216
"""Build the ``features_used`` snapshot for the compile event.
218217
219-
Every known feature ships with its invocation count, derived from a fresh
220-
walk of the live app at compile end. State, app, component, and config
221-
walks are the single source of truth — there are no marker writes to
222-
merge in.
218+
Every known key ships with a count (zero by default) derived from a fresh
219+
walk of the live app, its states, the compiled component tree, and the
220+
config.
223221
224222
Args:
225223
app: The compiled application.
224+
config: The active Reflex config.
226225
user_states: Pre-walked user state classes (shared with state stats).
227-
component_walk: Pre-computed component-tree aggregates.
226+
upload_count: Pre-computed count of ``Upload`` instances in the tree.
228227
229228
Returns:
230229
Dict of feature key -> invocation count.
231230
"""
232231
features: dict[FeatureName, int] = dict.fromkeys(_KNOWN_FEATURES, 0)
233232
_walk_state_features(features, user_states)
234233
_walk_app_features(features, app)
235-
features["upload_count"] = component_walk["upload_count"]
234+
features["upload_count"] = upload_count
236235
if _HAS_SQLALCHEMY:
237236
features["db_model_count"] = len(ModelRegistry.get_models())
238-
_record_config_attestations(features)
237+
_record_config_attestations(features, config)
239238
return features
240239

241240

@@ -261,7 +260,6 @@ def _walk_state_features(
261260
features: The snapshot to populate.
262261
user_states: Pre-walked user state classes.
263262
"""
264-
storage_counts: dict[FeatureName, int] = dict.fromkeys(_STORAGE_FEATURE.values(), 0)
265263
shared = background = 0
266264
for state_cls in user_states:
267265
if issubclass(state_cls, SharedState):
@@ -272,8 +270,7 @@ def _walk_state_features(
272270
for field in state_cls.get_fields().values():
273271
key = _storage_feature_for_field(field)
274272
if key is not None:
275-
storage_counts[key] += 1
276-
features.update(storage_counts)
273+
features[key] += 1
277274
features["shared_state_count"] = shared
278275
features["background_event_handlers_count"] = background
279276

@@ -286,7 +283,7 @@ def _walk_app_features(features: dict[FeatureName, int], app: App) -> None:
286283
app: The compiled application.
287284
"""
288285
features["dynamic_routes_count"] = sum(
289-
1 for route in app._unevaluated_pages if "[" in route
286+
1 for route in app._unevaluated_pages if get_route_args(route)
290287
)
291288
user_tasks = 0
292289
for task in app.get_lifespan_tasks():
@@ -296,30 +293,32 @@ def _walk_app_features(features: dict[FeatureName, int], app: App) -> None:
296293
features["lifespan_tasks_count"] = user_tasks
297294

298295

299-
def _record_config_attestations(features: dict[FeatureName, int]) -> None:
296+
def _record_config_attestations(
297+
features: dict[FeatureName, int], config: Config
298+
) -> None:
300299
"""Write config-derived feature counts (state-manager mode, CORS).
301300
302301
Args:
303302
features: The snapshot to populate.
303+
config: The active Reflex config.
304304
"""
305-
config = get_config()
306305
key = _STATE_MANAGER_FEATURE.get(config.state_manager_mode.value)
307306
if key is not None:
308307
features[key] = 1
309308
if tuple(config.cors_allowed_origins) != ("*",):
310309
features["cors_customized"] = 1
311310

312311

313-
def _storage_feature_for_field(field: Any) -> FeatureName | None:
312+
def _storage_feature_for_field(field: Field) -> FeatureName | None:
314313
"""Return the feature key for a client-storage state field, or None.
315314
316315
Mirrors ``BaseState._is_client_storage``: detected by an instance default
317-
or by a ``ClientStorageBase`` subclass annotation. Walks the MRO so user
318-
subclasses of ``Cookie`` / ``LocalStorage`` / ``SessionStorage`` are
319-
bucketed under their parent.
316+
or by a ``ClientStorageBase`` subclass annotation. User subclasses of
317+
``Cookie`` / ``LocalStorage`` / ``SessionStorage`` are bucketed under
318+
their parent via an MRO walk.
320319
321320
Args:
322-
field: A pydantic ``ModelField`` from ``state_cls.get_fields()``.
321+
field: A ``Field`` from ``state_cls.get_fields()``.
323322
324323
Returns:
325324
The feature key, or ``None`` if the field isn't a client-storage var.
@@ -331,6 +330,9 @@ def _storage_feature_for_field(field: Any) -> FeatureName | None:
331330
cls = field.type_
332331
else:
333332
return None
333+
direct = _STORAGE_FEATURE.get(cls)
334+
if direct is not None:
335+
return direct
334336
for ancestor in cls.__mro__:
335337
feature = _STORAGE_FEATURE.get(ancestor)
336338
if feature is not None:

0 commit comments

Comments
 (0)