55from reflex .utils import telemetry
66
77
8+ @pytest .fixture
9+ def event_defaults (mocker : MockerFixture ) -> dict :
10+ """Patch ``get_event_defaults()`` with a fresh dict.
11+
12+ Returns:
13+ The dict that ``get_event_defaults()`` is patched to return, so tests
14+ can assert it isn't mutated by the code under test.
15+ """
16+ defaults = {
17+ "api_key" : "test_api_key" ,
18+ "properties" : {
19+ "distinct_id" : 12345 ,
20+ "distinct_app_id" : 78285505863498957834586115958872998605 ,
21+ "user_os" : "Test OS" ,
22+ "user_os_detail" : "Mocked Platform" ,
23+ "reflex_version" : "0.8.0" ,
24+ "python_version" : "3.8.0" ,
25+ "node_version" : None ,
26+ "bun_version" : None ,
27+ "reflex_enterprise_version" : None ,
28+ "cpu_count" : 4 ,
29+ "memory" : 8192 ,
30+ "cpu_info" : {},
31+ },
32+ }
33+ mocker .patch ("reflex.utils.telemetry.get_event_defaults" , return_value = defaults )
34+ return defaults
35+
36+
37+ @pytest .fixture
38+ def httpx_post (mocker : MockerFixture ):
39+ """Mock ``httpx.post`` used by ``telemetry._send``.
40+
41+ Returns:
42+ The mock for ``httpx.post`` so tests can assert on the posted payload.
43+ """
44+ return mocker .patch ("httpx.post" )
45+
46+
847def test_telemetry ():
948 """Test that telemetry is sent correctly."""
1049 # Check that the user OS is one of the supported operating systems.
@@ -29,62 +68,106 @@ def test_disable():
2968 assert not telemetry ._send ("test" , telemetry_enabled = False )
3069
3170
32- @pytest .mark .parametrize ("event" , ["init" , "reinit" , "run-dev" , "run-prod" , "export" ])
33- def test_send (mocker : MockerFixture , event ):
34- httpx_post_mock = mocker .patch ("httpx.post" )
35-
36- # Mock _get_event_defaults to return a complete valid response
37- mock_defaults = {
38- "api_key" : "test_api_key" ,
39- "properties" : {
40- "distinct_id" : 12345 ,
41- "distinct_app_id" : 78285505863498957834586115958872998605 ,
42- "user_os" : "Test OS" ,
43- "user_os_detail" : "Mocked Platform" ,
44- "reflex_version" : "0.8.0" ,
45- "python_version" : "3.8.0" ,
46- "node_version" : None ,
47- "bun_version" : None ,
48- "reflex_enterprise_version" : None ,
49- "cpu_count" : 4 ,
50- "memory" : 8192 ,
51- "cpu_info" : {},
52- },
53- }
54- mocker .patch (
55- "reflex.utils.telemetry._get_event_defaults" , return_value = mock_defaults
71+ @pytest .mark .parametrize (
72+ ("event" , "kwargs" , "expected_props" ),
73+ [
74+ ("init" , {}, {}),
75+ ("reinit" , {}, {}),
76+ ("run-dev" , {}, {}),
77+ ("run-prod" , {}, {}),
78+ ("export" , {}, {}),
79+ (
80+ "export" ,
81+ {"status" : "success" , "duration" : 1.23 },
82+ {"status" : "success" , "duration" : 1.23 },
83+ ),
84+ (
85+ "export" ,
86+ {
87+ "status" : "failure" ,
88+ "detail" : "ValueError" ,
89+ "duration" : 0.5 ,
90+ "compile_duration" : 0.4 ,
91+ },
92+ {
93+ "status" : "failure" ,
94+ "detail" : "ValueError" ,
95+ "duration" : 0.5 ,
96+ "compile_duration" : 0.4 ,
97+ },
98+ ),
99+ ],
100+ )
101+ def test_send (event_defaults , httpx_post , event , kwargs , expected_props ):
102+ telemetry ._send (event , telemetry_enabled = True , ** kwargs )
103+ httpx_post .assert_called_once ()
104+ posted = httpx_post .call_args .kwargs ["json" ]
105+ assert posted ["event" ] == event
106+ for key , value in expected_props .items ():
107+ assert posted ["properties" ][key ] == value
108+
109+
110+ def test_send_does_not_leak_kwargs_between_events (event_defaults , httpx_post ):
111+ """Per-event kwargs must not leak into a subsequent event's payload."""
112+ telemetry ._send ("export" , telemetry_enabled = True , status = "success" , duration = 1.0 )
113+ telemetry ._send (
114+ "export" ,
115+ telemetry_enabled = True ,
116+ status = "failure" ,
117+ detail = "ValueError" ,
118+ duration = 2.0 ,
56119 )
57120
58- telemetry ._send (event , telemetry_enabled = True )
59- httpx_post_mock .assert_called_once ()
60-
61-
62- def _make_mock_defaults ():
63- return {
64- "api_key" : "test_api_key" ,
65- "properties" : {
66- "distinct_id" : 12345 ,
67- "distinct_app_id" : 78285505863498957834586115958872998605 ,
68- "user_os" : "Test OS" ,
69- "user_os_detail" : "Mocked Platform" ,
70- "reflex_version" : "0.8.0" ,
71- "python_version" : "3.8.0" ,
72- "node_version" : None ,
73- "bun_version" : None ,
74- "reflex_enterprise_version" : None ,
75- "cpu_count" : 4 ,
76- "memory" : 8192 ,
77- "cpu_info" : {},
78- },
79- }
80-
81-
82- def test_prepare_event_merges_properties (mocker : MockerFixture ):
83- mocker .patch (
84- "reflex.utils.telemetry._get_event_defaults" ,
85- return_value = _make_mock_defaults (),
121+ assert httpx_post .call_count == 2
122+ first_props = httpx_post .call_args_list [0 ].kwargs ["json" ]["properties" ]
123+ second_props = httpx_post .call_args_list [1 ].kwargs ["json" ]["properties" ]
124+
125+ assert first_props ["status" ] == "success"
126+ assert first_props ["duration" ] == pytest .approx (1.0 )
127+ assert "detail" not in first_props
128+
129+ assert second_props ["status" ] == "failure"
130+ assert second_props ["detail" ] == "ValueError"
131+ assert second_props ["duration" ] == pytest .approx (2.0 )
132+
133+ # The cached defaults must not have been polluted by either call.
134+ assert "status" not in event_defaults ["properties" ]
135+ assert "duration" not in event_defaults ["properties" ]
136+ assert "detail" not in event_defaults ["properties" ]
137+
138+
139+ def test_send_drops_unknown_kwargs (event_defaults , httpx_post ):
140+ """Unknown kwargs must not land in the posted payload."""
141+ telemetry ._send ("export" , telemetry_enabled = True , foo = "bar" , secret = "leak" )
142+ httpx_post .assert_called_once ()
143+ props = httpx_post .call_args .kwargs ["json" ]["properties" ]
144+ assert "foo" not in props
145+ assert "secret" not in props
146+
147+
148+ def test_send_drops_none_kwargs (event_defaults , httpx_post ):
149+ """None-valued kwargs for allowed keys are omitted from the posted payload."""
150+ telemetry ._send (
151+ "export" ,
152+ telemetry_enabled = True ,
153+ status = "success" ,
154+ detail = None ,
155+ duration = 0.1 ,
156+ compile_duration = None ,
157+ build_duration = 0.05 ,
158+ zip_duration = None ,
86159 )
160+ httpx_post .assert_called_once ()
161+ props = httpx_post .call_args .kwargs ["json" ]["properties" ]
162+ assert props ["status" ] == "success"
163+ assert props ["build_duration" ] == pytest .approx (0.05 )
164+ assert "detail" not in props
165+ assert "compile_duration" not in props
166+ assert "zip_duration" not in props
87167
168+
169+ def test_prepare_event_merges_properties (event_defaults ):
170+ """``properties`` payloads are merged into the event properties."""
88171 event = telemetry ._prepare_event (
89172 "compile" ,
90173 properties = {"pages_count" : 7 , "trigger" : "initial" },
@@ -99,35 +182,24 @@ def test_prepare_event_merges_properties(mocker: MockerFixture):
99182 assert props ["user_os" ] == "Test OS"
100183
101184
102- def test_prepare_event_does_not_mutate_cached_defaults (mocker : MockerFixture ):
185+ def test_prepare_event_does_not_mutate_cached_defaults (event_defaults ):
103186 """``_prepare_event`` must not mutate the @once_unless_none cached defaults."""
104- cached = _make_mock_defaults ()
105- mocker .patch (
106- "reflex.utils.telemetry._get_event_defaults" ,
107- return_value = cached ,
108- )
109-
110- cached_props_snapshot = dict (cached ["properties" ])
187+ cached_props_snapshot = dict (event_defaults ["properties" ])
111188
112189 telemetry ._prepare_event ("init" , template = "my-template" )
113190 telemetry ._prepare_event (
114191 "compile" ,
115192 properties = {"pages_count" : 3 , "duration_ms" : 42 },
116193 )
117194
118- assert cached ["properties" ] == cached_props_snapshot
119- assert "template" not in cached ["properties" ]
120- assert "pages_count" not in cached ["properties" ]
121- assert "duration_ms" not in cached ["properties" ]
195+ assert event_defaults ["properties" ] == cached_props_snapshot
196+ assert "template" not in event_defaults ["properties" ]
197+ assert "pages_count" not in event_defaults ["properties" ]
198+ assert "duration_ms" not in event_defaults ["properties" ]
122199
123200
124- def test_prepare_event_properties_override_kwargs (mocker : MockerFixture ):
201+ def test_prepare_event_properties_override_kwargs (event_defaults ):
125202 """If both kwargs and properties supply the same key, properties wins."""
126- mocker .patch (
127- "reflex.utils.telemetry._get_event_defaults" ,
128- return_value = _make_mock_defaults (),
129- )
130-
131203 event = telemetry ._prepare_event (
132204 "init" ,
133205 template = "from-kwarg" ,
0 commit comments