55from reflex .utils import telemetry
66
77
8+ def _mock_event_defaults () -> dict :
9+ return {
10+ "api_key" : "test_api_key" ,
11+ "properties" : {
12+ "distinct_id" : 12345 ,
13+ "distinct_app_id" : 78285505863498957834586115958872998605 ,
14+ "user_os" : "Test OS" ,
15+ "user_os_detail" : "Mocked Platform" ,
16+ "reflex_version" : "0.8.0" ,
17+ "python_version" : "3.8.0" ,
18+ "node_version" : None ,
19+ "bun_version" : None ,
20+ "reflex_enterprise_version" : None ,
21+ "cpu_count" : 4 ,
22+ "memory" : 8192 ,
23+ "cpu_info" : {},
24+ },
25+ }
26+
27+
28+ def _patch_event_defaults (mocker : MockerFixture , value ):
29+ """Replace the cached get_event_defaults() so it returns ``value``, bypassing the once_unless_none cache."""
30+ mocker .patch ("reflex.utils.telemetry.get_event_defaults" , return_value = value )
31+
32+
833def test_telemetry ():
934 """Test that telemetry is sent correctly."""
1035 # Check that the user OS is one of the supported operating systems.
@@ -29,31 +54,112 @@ def test_disable():
2954 assert not telemetry ._send ("test" , telemetry_enabled = False )
3055
3156
32- @pytest .mark .parametrize ("event" , ["init" , "reinit" , "run-dev" , "run-prod" , "export" ])
33- def test_send (mocker : MockerFixture , event ):
57+ @pytest .mark .parametrize (
58+ ("event" , "kwargs" , "expected_props" ),
59+ [
60+ ("init" , {}, {}),
61+ ("reinit" , {}, {}),
62+ ("run-dev" , {}, {}),
63+ ("run-prod" , {}, {}),
64+ ("export" , {}, {}),
65+ (
66+ "export" ,
67+ {"status" : "success" , "duration" : 1.23 },
68+ {"status" : "success" , "duration" : 1.23 },
69+ ),
70+ (
71+ "export" ,
72+ {
73+ "status" : "failure" ,
74+ "detail" : "ValueError" ,
75+ "duration" : 0.5 ,
76+ "compile_duration" : 0.4 ,
77+ },
78+ {
79+ "status" : "failure" ,
80+ "detail" : "ValueError" ,
81+ "duration" : 0.5 ,
82+ "compile_duration" : 0.4 ,
83+ },
84+ ),
85+ ],
86+ )
87+ def test_send (mocker : MockerFixture , event , kwargs , expected_props ):
3488 httpx_post_mock = mocker .patch ("httpx.post" )
89+ _patch_event_defaults (mocker , _mock_event_defaults ())
3590
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
91+ telemetry ._send (event , telemetry_enabled = True , ** kwargs )
92+ httpx_post_mock .assert_called_once ()
93+ posted = httpx_post_mock .call_args .kwargs ["json" ]
94+ assert posted ["event" ] == event
95+ for key , value in expected_props .items ():
96+ assert posted ["properties" ][key ] == value
97+
98+
99+ def test_send_does_not_leak_kwargs_between_events (mocker : MockerFixture ):
100+ """Per-event kwargs must not leak into a subsequent event's payload."""
101+ httpx_post_mock = mocker .patch ("httpx.post" )
102+ defaults = _mock_event_defaults ()
103+ _patch_event_defaults (mocker , defaults )
104+
105+ telemetry ._send ("export" , telemetry_enabled = True , status = "success" , duration = 1.0 )
106+ telemetry ._send (
107+ "export" ,
108+ telemetry_enabled = True ,
109+ status = "failure" ,
110+ detail = "ValueError" ,
111+ duration = 2.0 ,
56112 )
57113
58- telemetry ._send (event , telemetry_enabled = True )
114+ assert httpx_post_mock .call_count == 2
115+ first_props = httpx_post_mock .call_args_list [0 ].kwargs ["json" ]["properties" ]
116+ second_props = httpx_post_mock .call_args_list [1 ].kwargs ["json" ]["properties" ]
117+
118+ assert first_props ["status" ] == "success"
119+ assert first_props ["duration" ] == pytest .approx (1.0 )
120+ assert "detail" not in first_props
121+
122+ assert second_props ["status" ] == "failure"
123+ assert second_props ["detail" ] == "ValueError"
124+ assert second_props ["duration" ] == pytest .approx (2.0 )
125+
126+ # The cached defaults must not have been polluted by either call.
127+ assert "status" not in defaults ["properties" ]
128+ assert "duration" not in defaults ["properties" ]
129+ assert "detail" not in defaults ["properties" ]
130+
131+
132+ def test_send_drops_unknown_kwargs (mocker : MockerFixture ):
133+ """Unknown kwargs must not land in the posted payload."""
134+ httpx_post_mock = mocker .patch ("httpx.post" )
135+ _patch_event_defaults (mocker , _mock_event_defaults ())
136+
137+ telemetry ._send ("export" , telemetry_enabled = True , foo = "bar" , secret = "leak" )
138+ httpx_post_mock .assert_called_once ()
139+ props = httpx_post_mock .call_args .kwargs ["json" ]["properties" ]
140+ assert "foo" not in props
141+ assert "secret" not in props
142+
143+
144+ def test_send_drops_none_kwargs (mocker : MockerFixture ):
145+ """None-valued kwargs for allowed keys are omitted from the posted payload."""
146+ httpx_post_mock = mocker .patch ("httpx.post" )
147+ _patch_event_defaults (mocker , _mock_event_defaults ())
148+
149+ telemetry ._send (
150+ "export" ,
151+ telemetry_enabled = True ,
152+ status = "success" ,
153+ detail = None ,
154+ duration = 0.1 ,
155+ compile_duration = None ,
156+ build_duration = 0.05 ,
157+ zip_duration = None ,
158+ )
59159 httpx_post_mock .assert_called_once ()
160+ props = httpx_post_mock .call_args .kwargs ["json" ]["properties" ]
161+ assert props ["status" ] == "success"
162+ assert props ["build_duration" ] == pytest .approx (0.05 )
163+ assert "detail" not in props
164+ assert "compile_duration" not in props
165+ assert "zip_duration" not in props
0 commit comments