33from __future__ import annotations
44
55from collections .abc import Iterable , Iterator
6- from typing import TYPE_CHECKING , Any , TypedDict
6+ from typing import TYPE_CHECKING , TypedDict
77
88from reflex_base .config import get_config
99from reflex_base .utils import console
1010
1111from reflex .utils import telemetry
12+ from reflex .utils .telemetry_context import (
13+ _KNOWN_FEATURES ,
14+ TelemetryContext ,
15+ _recorded_features ,
16+ increment_feature ,
17+ )
18+
19+ __all__ = ["increment_feature" , "record_compile" ]
1220
1321if TYPE_CHECKING :
1422 from reflex_base .components .component import BaseComponent
1523
1624 from reflex .app import App
1725 from reflex .state import BaseState
18- from reflex .utils .telemetry_context import CompileTrigger , TelemetryContext
26+ from reflex .utils .telemetry_context import CompileTrigger , FeatureName
1927
2028
2129class _StateStats (TypedDict ):
@@ -42,7 +50,7 @@ class _CompileEventProperties(TypedDict):
4250 pages_count : int
4351 component_counts : dict [str , int ]
4452 states : list [_StateStats ]
45- features_used : dict [str , Any ]
53+ features_used : dict [FeatureName , int ]
4654 duration_ms : int
4755 trigger : CompileTrigger | None
4856 exception : _ExceptionInfo | None
@@ -78,13 +86,14 @@ def _collect_compile_event_payload(
7886 The properties dict to send to PostHog.
7987 """
8088 config = get_config ()
89+ user_states = list (_walk_states (app ._state ))
8190 return {
8291 "plugins_enabled" : [p .__class__ .__name__ for p in config .plugins ],
8392 "plugins_disabled" : [p .__name__ for p in config .disable_plugins ],
8493 "pages_count" : len (app ._pages ),
8594 "component_counts" : _count_components (app ._pages .values ()),
86- "states" : _collect_all_state_stats ( app ) ,
87- "features_used" : dict (ctx . features_used ),
95+ "states" : [ _collect_state_stats ( s ) for s in user_states ] ,
96+ "features_used" : _collect_features_used (ctx , user_states ),
8897 "duration_ms" : ctx .elapsed_ms (),
8998 "trigger" : ctx .trigger ,
9099 "exception" : _sanitize_exception (ctx .exception ),
@@ -141,18 +150,6 @@ def _walk_states(root: type[BaseState] | None) -> Iterator[type[BaseState]]:
141150 yield from _walk_states (sub )
142151
143152
144- def _collect_all_state_stats (app : App ) -> list [_StateStats ]:
145- """Collect per-state statistics for every state attached to the app.
146-
147- Args:
148- app: The compiled application.
149-
150- Returns:
151- A list of per-state stat dicts.
152- """
153- return [_collect_state_stats (state_cls ) for state_cls in _walk_states (app ._state )]
154-
155-
156153def _collect_state_stats (state_cls : type [BaseState ]) -> _StateStats :
157154 """Collect structural statistics for a single state class.
158155
@@ -191,3 +188,64 @@ def _sanitize_exception(exc: BaseException | None) -> _ExceptionInfo | None:
191188 if exc is None :
192189 return None
193190 return {"type" : type (exc ).__name__ }
191+
192+
193+ def _collect_features_used (
194+ ctx : TelemetryContext , user_states : list [type [BaseState ]]
195+ ) -> dict [FeatureName , int ]:
196+ """Build the ``features_used`` snapshot for the compile event.
197+
198+ Every known feature ships with its invocation count (zero by default), so
199+ consumers don't have to distinguish "feature not used" from "detector broken."
200+
201+ Args:
202+ ctx: The active telemetry context.
203+ user_states: Pre-walked user state classes (shared with state stats).
204+
205+ Returns:
206+ Dict of feature key -> invocation count.
207+ """
208+ features : dict [FeatureName , int ] = dict .fromkeys (_KNOWN_FEATURES , 0 )
209+ features .update (_recorded_features )
210+ _record_config_attestations (features )
211+ _record_background_handler_count (features , user_states )
212+ features .update (ctx .features_used )
213+ return features
214+
215+
216+ _STATE_MANAGER_FEATURE : dict [str , FeatureName ] = {
217+ "disk" : "state_manager_disk" ,
218+ "memory" : "state_manager_memory" ,
219+ "redis" : "state_manager_redis" ,
220+ }
221+
222+
223+ def _record_config_attestations (features : dict [FeatureName , int ]) -> None :
224+ """Write config-derived feature counts (state-manager mode, CORS).
225+
226+ Args:
227+ features: The snapshot to populate.
228+ """
229+ config = get_config ()
230+ key = _STATE_MANAGER_FEATURE .get (config .state_manager_mode .value )
231+ if key is not None :
232+ features [key ] = 1
233+ if tuple (config .cors_allowed_origins ) != ("*" ,):
234+ features ["cors_customized" ] = 1
235+
236+
237+ def _record_background_handler_count (
238+ features : dict [FeatureName , int ], user_states : list [type [BaseState ]]
239+ ) -> None :
240+ """Count ``@rx.event(background=True)`` handlers across user states.
241+
242+ Args:
243+ features: The snapshot to populate.
244+ user_states: Pre-walked user state classes.
245+ """
246+ features ["background_event_handlers_count" ] = sum (
247+ 1
248+ for state_cls in user_states
249+ for handler in state_cls .event_handlers .values ()
250+ if handler .is_background
251+ )
0 commit comments