44
55from collections .abc import Iterable , Iterator
66from 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
1010from reflex_base .telemetry_context import _KNOWN_FEATURES , TelemetryContext
1111from reflex_base .utils import console
1212from reflex_components_core .core .upload import Upload
1919 SessionStorage ,
2020)
2121from reflex .model import ModelRegistry
22+ from reflex .route import get_route_args
2223from reflex .utils import telemetry
2324
2425_HAS_SQLALCHEMY = find_spec ("sqlalchemy" ) is not None
2829if 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-
7368def 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
150148def _walk_states (root : type [BaseState ] | None ) -> Iterator [type [BaseState ]]:
@@ -211,31 +209,32 @@ def _sanitize_exception(exc: BaseException | None) -> _ExceptionInfo | None:
211209
212210def _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